license switch agpl -> busl1, sprints work, new product advisories

This commit is contained in:
master
2026-01-20 15:32:20 +02:00
parent 4903395618
commit c32fff8f86
1835 changed files with 38630 additions and 4359 deletions

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_4100_0006_0005 - Admin Utility Integration
using System.CommandLine;

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_4100_0006_0005 - Admin Utility Integration
using System.Net.Http.Json;

View File

@@ -1,5 +1,5 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the AGPL-3.0-or-later license.
// Licensed under the BUSL-1.1 license.
using System;
using System.CommandLine;

View File

@@ -1,5 +1,5 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the AGPL-3.0-or-later license.
// Licensed under the BUSL-1.1 license.
using System;
using System.IO;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: BUSL-1.1
using System.CommandLine;
using StellaOps.Agent.Core.Bootstrap;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: BUSL-1.1
using System.CommandLine;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: BUSL-1.1
using System.CommandLine;
using System.Text.Json;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: BUSL-1.1
using System.CommandLine;
using System.Text.Json;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) Stella Ops. All rights reserved. SPDX-License-Identifier: BUSL-1.1
using System.CommandLine;

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------------
// BundleCommandGroup.cs
// Sprint: SPRINT_20260120_029_AirGap_offline_bundle_contract
// Task: TASK-029-003 - Signed verification report generation
// Description: Domain-level bundle command group for offline evidence bundles.
// -----------------------------------------------------------------------------
using System.CommandLine;
namespace StellaOps.Cli.Commands;
public static class BundleCommandGroup
{
public static Command BuildBundleCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var bundle = new Command("bundle", "Offline evidence bundle operations.");
bundle.Add(BundleVerifyCommand.BuildVerifyBundleEnhancedCommand(
services,
verboseOption,
cancellationToken));
return bundle;
}
}

View File

@@ -8,10 +8,16 @@
using System.CommandLine;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.Core.Predicates;
using StellaOps.Attestor.Core.Signing;
using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.Serialization;
using StellaOps.Cryptography;
namespace StellaOps.Cli.Commands;
@@ -29,7 +35,7 @@ public static class BundleVerifyCommand
};
/// <summary>
/// Builds the 'verify --bundle' enhanced command.
/// Builds the 'bundle verify' enhanced command.
/// </summary>
public static Command BuildVerifyBundleEnhancedCommand(
IServiceProvider services,
@@ -65,10 +71,20 @@ public static class BundleVerifyCommand
var strictOption = new Option<bool>("--strict")
{
Description = "Fail on any warning (missing optional artifacts)"
Description = "Fail on any warning (missing optional artifacts)"
};
var command = new Command("bundle-verify", "Verify offline evidence bundle with full cryptographic verification")
var signerOption = new Option<string?>("--signer")
{
Description = "Path to signing key (PEM) for DSSE verification report"
};
var signerCertOption = new Option<string?>("--signer-cert")
{
Description = "Path to signer certificate PEM (optional; embedded in report metadata)"
};
var command = new Command("verify", "Verify offline evidence bundle with full cryptographic verification")
{
bundleOption,
trustRootOption,
@@ -76,6 +92,8 @@ public static class BundleVerifyCommand
offlineOption,
outputOption,
strictOption,
signerOption,
signerCertOption,
verboseOption
};
@@ -87,6 +105,8 @@ public static class BundleVerifyCommand
var offline = parseResult.GetValue(offlineOption);
var output = parseResult.GetValue(outputOption) ?? "table";
var strict = parseResult.GetValue(strictOption);
var signer = parseResult.GetValue(signerOption);
var signerCert = parseResult.GetValue(signerCertOption);
var verbose = parseResult.GetValue(verboseOption);
return await HandleVerifyBundleAsync(
@@ -97,6 +117,8 @@ public static class BundleVerifyCommand
offline,
output,
strict,
signer,
signerCert,
verbose,
cancellationToken);
});
@@ -112,6 +134,8 @@ public static class BundleVerifyCommand
bool offline,
string outputFormat,
bool strict,
string? signerKeyPath,
string? signerCertPath,
bool verbose,
CancellationToken ct)
{
@@ -125,6 +149,9 @@ public static class BundleVerifyCommand
Offline = offline
};
string? bundleDir = null;
BundleManifestDto? manifest = null;
try
{
if (outputFormat != "json")
@@ -136,18 +163,29 @@ public static class BundleVerifyCommand
}
// Step 1: Extract/read bundle
var bundleDir = await ExtractBundleAsync(bundlePath, ct);
bundleDir = await ExtractBundleAsync(bundlePath, ct);
// Step 2: Parse manifest
var manifestPath = Path.Combine(bundleDir, "manifest.json");
if (!File.Exists(manifestPath))
{
result.Checks.Add(new VerificationCheck("manifest", false, "manifest.json not found"));
return OutputResult(result, outputFormat, strict);
return await FinalizeResultAsync(
result,
manifest,
bundleDir,
trustRoot,
rekorCheckpoint,
offline,
outputFormat,
strict,
signerKeyPath,
signerCertPath,
ct);
}
var manifestJson = await File.ReadAllTextAsync(manifestPath, ct);
var manifest = JsonSerializer.Deserialize<BundleManifestDto>(manifestJson, JsonOptions);
var manifestJson = await File.ReadAllTextAsync(manifestPath, ct);
manifest = JsonSerializer.Deserialize<BundleManifestDto>(manifestJson, JsonOptions);
result.Checks.Add(new VerificationCheck("manifest", true, "manifest.json parsed successfully"));
result.SchemaVersion = manifest?.SchemaVersion;
result.Image = manifest?.Bundle?.Image;
@@ -185,11 +223,18 @@ public static class BundleVerifyCommand
Console.WriteLine($"Step 5: Payload Types {(payloadsPassed ? "" : "")}");
}
result.CompletedAt = DateTimeOffset.UtcNow;
result.OverallStatus = result.Checks.All(c => c.Passed) ? "PASSED" :
result.Checks.Any(c => !c.Passed && c.Severity == "error") ? "FAILED" : "PASSED_WITH_WARNINGS";
return OutputResult(result, outputFormat, strict);
return await FinalizeResultAsync(
result,
manifest,
bundleDir,
trustRoot,
rekorCheckpoint,
offline,
outputFormat,
strict,
signerKeyPath,
signerCertPath,
ct);
}
catch (Exception ex)
{
@@ -446,6 +491,377 @@ public static class BundleVerifyCommand
return true;
}
private static async Task<int> FinalizeResultAsync(
VerificationResult result,
BundleManifestDto? manifest,
string bundleDir,
string? trustRoot,
string? rekorCheckpoint,
bool offline,
string outputFormat,
bool strict,
string? signerKeyPath,
string? signerCertPath,
CancellationToken ct)
{
result.CompletedAt ??= DateTimeOffset.UtcNow;
if (!string.IsNullOrWhiteSpace(signerKeyPath))
{
var outcome = await TryWriteSignedReportAsync(
result,
manifest,
bundleDir,
trustRoot,
rekorCheckpoint,
offline,
signerKeyPath,
signerCertPath,
ct);
if (outcome.Success)
{
result.SignedReportPath = outcome.ReportPath;
result.SignerKeyId = outcome.KeyId;
result.SignerAlgorithm = outcome.Algorithm;
result.SignedAt = outcome.SignedAt;
result.Checks.Add(new VerificationCheck(
"report:signature",
true,
$"Signed report written to {outcome.ReportPath}"));
}
else
{
result.Checks.Add(new VerificationCheck(
"report:signature",
false,
outcome.Error ?? "Signed report generation failed")
{
Severity = "error"
});
}
}
result.OverallStatus = ComputeOverallStatus(result.Checks);
return OutputResult(result, outputFormat, strict);
}
private static async Task<SignedReportOutcome> TryWriteSignedReportAsync(
VerificationResult result,
BundleManifestDto? manifest,
string bundleDir,
string? trustRoot,
string? rekorCheckpoint,
bool offline,
string signerKeyPath,
string? signerCertPath,
CancellationToken ct)
{
try
{
var signingKey = LoadSigningKey(signerKeyPath);
var signerCert = await LoadSignerCertificateAsync(signerCertPath, signerKeyPath, ct);
var report = BuildVerificationReport(result, manifest, trustRoot, rekorCheckpoint, offline);
var signer = new DsseVerificationReportSigner(new EnvelopeSignatureService());
var signedAt = result.CompletedAt ?? DateTimeOffset.UtcNow;
var signResult = await signer.SignAsync(new VerificationReportSigningRequest(
report,
signingKey,
signerCert,
signedAt), ct);
var outputDir = Path.Combine(bundleDir, "out");
Directory.CreateDirectory(outputDir);
var reportPath = Path.Combine(outputDir, "verification.report.json");
await File.WriteAllTextAsync(reportPath, signResult.EnvelopeJson, ct);
return new SignedReportOutcome(
true,
reportPath,
signingKey.KeyId,
signingKey.AlgorithmId,
signResult.Report.Verifier?.SignedAt,
null);
}
catch (Exception ex)
{
return new SignedReportOutcome(false, null, null, null, null, ex.Message);
}
}
private static VerificationReportPredicate BuildVerificationReport(
VerificationResult result,
BundleManifestDto? manifest,
string? trustRoot,
string? rekorCheckpoint,
bool offline)
{
var steps = result.Checks
.Select((check, index) => new VerificationStep
{
Step = index + 1,
Name = check.Name,
Status = MapStepStatus(check),
DurationMs = 0,
Details = check.Message,
Issues = BuildIssues(check)
})
.ToArray();
var summary = ComputeOverallStatus(result.Checks);
var overallStatus = MapOverallStatus(summary);
var overall = new OverallVerificationResult
{
Status = overallStatus,
Summary = summary,
TotalDurationMs = (long?)((result.CompletedAt - result.StartedAt)?.TotalMilliseconds) ?? 0,
PassedSteps = steps.Count(step => step.Status == VerificationStepStatus.Passed),
FailedSteps = steps.Count(step => step.Status == VerificationStepStatus.Failed),
WarningSteps = steps.Count(step => step.Status == VerificationStepStatus.Warning),
SkippedSteps = steps.Count(step => step.Status == VerificationStepStatus.Skipped)
};
TrustChainInfo? trustChain = null;
if (!string.IsNullOrWhiteSpace(trustRoot) || !string.IsNullOrWhiteSpace(rekorCheckpoint))
{
var rekorVerified = result.Checks.Any(check =>
string.Equals(check.Name, "rekor:inclusion", StringComparison.OrdinalIgnoreCase) && check.Passed);
trustChain = new TrustChainInfo
{
RootOfTrust = trustRoot,
RekorVerified = rekorVerified,
RekorLogIndex = null,
TsaVerified = false,
Timestamp = null,
SignerIdentity = result.SignerKeyId
};
}
return new VerificationReportPredicate
{
ReportId = ComputeReportId(result, manifest),
GeneratedAt = result.CompletedAt ?? DateTimeOffset.UtcNow,
Generator = new GeneratorInfo
{
Tool = "stella bundle verify",
Version = GetCliVersion()
},
Subject = new VerificationSubject
{
BundleId = manifest?.CanonicalManifestHash,
BundleDigest = manifest?.Subject?.Sha256,
ArtifactDigest = manifest?.Bundle?.Digest,
ArtifactName = manifest?.Bundle?.Image
},
VerificationSteps = steps,
OverallResult = overall,
TrustChain = trustChain,
ReplayMode = offline ? "offline" : "online"
};
}
private static VerificationStepStatus MapStepStatus(VerificationCheck check)
{
if (!check.Passed)
{
return VerificationStepStatus.Failed;
}
return check.Severity switch
{
"warning" => VerificationStepStatus.Warning,
"info" => VerificationStepStatus.Passed,
_ => VerificationStepStatus.Passed
};
}
private static IReadOnlyList<VerificationIssue>? BuildIssues(VerificationCheck check)
{
if (check.Passed && !string.Equals(check.Severity, "warning", StringComparison.OrdinalIgnoreCase))
{
return null;
}
return new[]
{
new VerificationIssue
{
Severity = MapIssueSeverity(check),
Code = check.Name,
Message = check.Message
}
};
}
private static IssueSeverity MapIssueSeverity(VerificationCheck check)
{
if (!check.Passed)
{
return IssueSeverity.Error;
}
return string.Equals(check.Severity, "warning", StringComparison.OrdinalIgnoreCase)
? IssueSeverity.Warning
: IssueSeverity.Info;
}
private static VerificationStepStatus MapOverallStatus(string? status)
{
return status switch
{
"PASSED" => VerificationStepStatus.Passed,
"FAILED" => VerificationStepStatus.Failed,
"PASSED_WITH_WARNINGS" => VerificationStepStatus.Warning,
_ => VerificationStepStatus.Skipped
};
}
private static string ComputeOverallStatus(IReadOnlyList<VerificationCheck> checks)
{
if (checks.Count == 0)
{
return "UNKNOWN";
}
if (checks.All(check => check.Passed))
{
return "PASSED";
}
return checks.Any(check => !check.Passed && check.Severity == "error")
? "FAILED"
: "PASSED_WITH_WARNINGS";
}
private static string ComputeReportId(VerificationResult result, BundleManifestDto? manifest)
{
if (!string.IsNullOrWhiteSpace(manifest?.CanonicalManifestHash))
{
return manifest.CanonicalManifestHash!;
}
if (!string.IsNullOrWhiteSpace(manifest?.Subject?.Sha256))
{
return manifest.Subject.Sha256!;
}
return ComputeSha256Hex(result.BundlePath);
}
private static string ComputeSha256Hex(string value)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value ?? string.Empty));
return $"sha256:{Convert.ToHexString(bytes).ToLowerInvariant()}";
}
private static EnvelopeKey LoadSigningKey(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new InvalidOperationException("Signing key path is required for report signing.");
}
if (!File.Exists(path))
{
throw new FileNotFoundException($"Signing key file not found: {path}");
}
var pem = File.ReadAllText(path);
using var ecdsa = ECDsa.Create();
try
{
ecdsa.ImportFromPem(pem);
}
catch (CryptographicException ex)
{
throw new InvalidOperationException("Failed to load ECDSA private key from PEM.", ex);
}
var parameters = ecdsa.ExportParameters(true);
var algorithm = ResolveEcdsaAlgorithm(ecdsa.KeySize);
return EnvelopeKey.CreateEcdsaSigner(algorithm, parameters);
}
private static string ResolveEcdsaAlgorithm(int keySize)
{
return keySize switch
{
256 => SignatureAlgorithms.Es256,
384 => SignatureAlgorithms.Es384,
521 => SignatureAlgorithms.Es512,
_ => throw new InvalidOperationException($"Unsupported ECDSA key size: {keySize}.")
};
}
private static async Task<string?> LoadSignerCertificateAsync(
string? signerCertPath,
string signerKeyPath,
CancellationToken ct)
{
if (!string.IsNullOrWhiteSpace(signerCertPath))
{
if (!File.Exists(signerCertPath))
{
throw new FileNotFoundException($"Signer certificate file not found: {signerCertPath}");
}
var certPem = await File.ReadAllTextAsync(signerCertPath, ct);
return NormalizePem(certPem);
}
var keyPem = await File.ReadAllTextAsync(signerKeyPath, ct);
return ExtractCertificatePem(keyPem);
}
private static string? ExtractCertificatePem(string pem)
{
const string beginMarker = "-----BEGIN CERTIFICATE-----";
const string endMarker = "-----END CERTIFICATE-----";
var builder = new StringBuilder();
var startIndex = 0;
while (true)
{
var begin = pem.IndexOf(beginMarker, startIndex, StringComparison.Ordinal);
if (begin < 0)
{
break;
}
var end = pem.IndexOf(endMarker, begin, StringComparison.Ordinal);
if (end < 0)
{
break;
}
var block = pem.Substring(begin, end - begin + endMarker.Length).Trim();
if (builder.Length > 0)
{
builder.Append('\n');
}
builder.Append(block);
startIndex = end + endMarker.Length;
}
return builder.Length == 0 ? null : NormalizePem(builder.ToString());
}
private static string? NormalizePem(string? pem)
{
if (string.IsNullOrWhiteSpace(pem))
{
return null;
}
return pem.Replace("\r\n", "\n").Trim();
}
private static string GetCliVersion()
{
return typeof(BundleVerifyCommand).Assembly.GetName().Version?.ToString() ?? "unknown";
}
private static int OutputResult(VerificationResult result, string format, bool strict)
{
if (format == "json")
@@ -472,6 +888,18 @@ public static class BundleVerifyCommand
Console.WriteLine();
Console.WriteLine($"Duration: {(result.CompletedAt - result.StartedAt)?.TotalMilliseconds:F0}ms");
if (!string.IsNullOrWhiteSpace(result.SignedReportPath))
{
Console.WriteLine($"Signed report: {result.SignedReportPath}");
if (!string.IsNullOrWhiteSpace(result.SignerKeyId))
{
var algo = string.IsNullOrWhiteSpace(result.SignerAlgorithm)
? string.Empty
: $" ({result.SignerAlgorithm})";
Console.WriteLine($"Signer key: {result.SignerKeyId}{algo}");
}
}
}
// Exit code
@@ -509,6 +937,18 @@ public static class BundleVerifyCommand
[JsonPropertyName("image")]
public string? Image { get; set; }
[JsonPropertyName("signedReportPath")]
public string? SignedReportPath { get; set; }
[JsonPropertyName("signerKeyId")]
public string? SignerKeyId { get; set; }
[JsonPropertyName("signerAlgorithm")]
public string? SignerAlgorithm { get; set; }
[JsonPropertyName("signedAt")]
public DateTimeOffset? SignedAt { get; set; }
[JsonPropertyName("checks")]
public List<VerificationCheck> Checks { get; set; } = [];
}
@@ -538,11 +978,25 @@ public static class BundleVerifyCommand
public string Severity { get; set; } = "info";
}
private sealed record SignedReportOutcome(
bool Success,
string? ReportPath,
string? KeyId,
string? Algorithm,
DateTimeOffset? SignedAt,
string? Error);
private sealed class BundleManifestDto
{
[JsonPropertyName("canonicalManifestHash")]
public string? CanonicalManifestHash { get; set; }
[JsonPropertyName("schemaVersion")]
public string? SchemaVersion { get; set; }
[JsonPropertyName("subject")]
public BundleSubjectDto? Subject { get; set; }
[JsonPropertyName("bundle")]
public BundleInfoDto? Bundle { get; set; }
@@ -550,11 +1004,23 @@ public static class BundleVerifyCommand
public VerifySectionDto? Verify { get; set; }
}
private sealed class BundleSubjectDto
{
[JsonPropertyName("sha256")]
public string? Sha256 { get; set; }
[JsonPropertyName("sha512")]
public string? Sha512 { get; set; }
}
private sealed class BundleInfoDto
{
[JsonPropertyName("image")]
public string? Image { get; set; }
[JsonPropertyName("digest")]
public string? Digest { get; set; }
[JsonPropertyName("artifacts")]
public List<ArtifactDto>? Artifacts { get; set; }
}

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20251226_007_BE_determinism_gaps
// Task: DET-GAP-08 - Exit codes for sign commands

View File

@@ -81,6 +81,8 @@ internal static class CommandFactory
root.Add(AdminCommandGroup.BuildAdminCommand(services, verboseOption, cancellationToken));
root.Add(BuildExportCommand(services, verboseOption, cancellationToken));
root.Add(BuildAttestCommand(services, verboseOption, cancellationToken));
root.Add(BundleCommandGroup.BuildBundleCommand(services, verboseOption, cancellationToken));
root.Add(TimestampCommandGroup.BuildTimestampCommand(verboseOption, cancellationToken));
root.Add(BuildRiskProfileCommand(verboseOption, cancellationToken));
root.Add(BuildAdvisoryCommand(services, verboseOption, cancellationToken));
root.Add(BuildForensicCommand(services, verboseOption, cancellationToken));
@@ -154,6 +156,9 @@ internal static class CommandFactory
// Sprint: Doctor Diagnostics System
root.Add(DoctorCommandGroup.BuildDoctorCommand(services, verboseOption, cancellationToken));
// Sprint: SPRINT_20260119_010_Attestor_tst_integration - RFC-3161 Timestamp commands
root.Add(TimestampCommandGroup.BuildTimestampCommand(verboseOption, cancellationToken));
// Sprint: SPRINT_20260117_026_CLI_why_blocked_command - Explain block decisions (M2 moat)
root.Add(ExplainCommandGroup.BuildExplainCommand(services, verboseOption, cancellationToken));
@@ -6619,6 +6624,14 @@ flowchart TB
{
Description = "Include detailed explanations for each verification check."
};
var requireTimestampOption = new Option<bool>("--require-timestamp")
{
Description = "Require RFC-3161 timestamp evidence in the attestation."
};
var maxSkewOption = new Option<string?>("--max-skew")
{
Description = "Maximum allowed timestamp skew (e.g., 5m, 30s)."
};
verify.Add(envelopeOption);
verify.Add(policyOption);
@@ -6627,6 +6640,8 @@ flowchart TB
verify.Add(verifyOutputOption);
verify.Add(verifyFormatOption);
verify.Add(verifyExplainOption);
verify.Add(requireTimestampOption);
verify.Add(maxSkewOption);
verify.SetAction((parseResult, _) =>
{
@@ -6637,9 +6652,23 @@ flowchart TB
var output = parseResult.GetValue(verifyOutputOption);
var format = parseResult.GetValue(verifyFormatOption) ?? "table";
var explain = parseResult.GetValue(verifyExplainOption);
var requireTimestamp = parseResult.GetValue(requireTimestampOption);
var maxSkew = parseResult.GetValue(maxSkewOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleAttestVerifyAsync(services, envelope, policy, root, checkpoint, output, format, explain, verbose, cancellationToken);
return CommandHandlers.HandleAttestVerifyAsync(
services,
envelope,
policy,
root,
checkpoint,
output,
format,
explain,
requireTimestamp,
maxSkew,
verbose,
cancellationToken);
});
// attest list (CLI-ATTEST-74-001)
@@ -6769,6 +6798,14 @@ flowchart TB
{
Description = "Explicitly skip Rekor submission."
};
var timestampOption = new Option<bool>("--timestamp")
{
Description = "Request RFC-3161 timestamping for the attestation."
};
var tsaOption = new Option<string?>("--tsa")
{
Description = "TSA URL for RFC-3161 timestamp requests."
};
var signOutputOption = new Option<string?>("--output", new[] { "-o" })
{
Description = "Output path for the signed DSSE envelope JSON."
@@ -6786,6 +6823,8 @@ flowchart TB
sign.Add(keylessOption);
sign.Add(transparencyLogOption);
sign.Add(noRekorOption);
sign.Add(timestampOption);
sign.Add(tsaOption);
sign.Add(signOutputOption);
sign.Add(signFormatOption);
@@ -6799,6 +6838,8 @@ flowchart TB
var keyless = parseResult.GetValue(keylessOption);
var useRekor = parseResult.GetValue(transparencyLogOption);
var noRekor = parseResult.GetValue(noRekorOption);
var includeTimestamp = parseResult.GetValue(timestampOption);
var tsaUrl = parseResult.GetValue(tsaOption);
var output = parseResult.GetValue(signOutputOption);
var format = parseResult.GetValue(signFormatOption) ?? "dsse";
var verbose = parseResult.GetValue(verboseOption);
@@ -6812,6 +6853,8 @@ flowchart TB
keyId,
keyless,
useRekor && !noRekor,
includeTimestamp,
tsaUrl,
output,
format,
verbose,

View File

@@ -1,5 +1,5 @@
// <copyright file="CommandHandlers.Config.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_014_CLI_config_viewer (CLI-CONFIG-010, CLI-CONFIG-011, CLI-CONFIG-012, CLI-CONFIG-013)
// </copyright>

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20251226_007_BE_determinism_gaps
// Task: DET-GAP-08 - CLI handlers for keyless signing

View File

@@ -1,5 +1,5 @@
// <copyright file="CommandHandlers.VerifyBundle.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
// </copyright>
using System.Collections.Immutable;

View File

@@ -19,6 +19,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Spectre.Console;
using Spectre.Console.Rendering;
@@ -34,6 +35,7 @@ using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Services.Models.Bun;
using StellaOps.Cli.Services.Models.Ruby;
using StellaOps.Cli.Telemetry;
using StellaOps.Attestor.Timestamping;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Kms;
@@ -10214,6 +10216,8 @@ internal static partial class CommandHandlers
string? outputPath,
string format,
bool explain,
bool requireTimestamp,
string? maxSkew,
bool verbose,
CancellationToken cancellationToken)
{
@@ -10233,6 +10237,27 @@ internal static partial class CommandHandlers
return ExitInputError;
}
TimeSpan? maxSkewDuration = null;
if (!string.IsNullOrWhiteSpace(maxSkew))
{
if (!TryParseRelativeDuration(maxSkew, out var parsedSkew) &&
!TimeSpan.TryParse(maxSkew, CultureInfo.InvariantCulture, out parsedSkew))
{
AnsiConsole.MarkupLine("[red]Error:[/] Invalid --max-skew value. Use duration (e.g., 5m, 30s, 2h).");
CliMetrics.RecordAttestVerify("input_error");
return ExitInputError;
}
if (parsedSkew <= TimeSpan.Zero)
{
AnsiConsole.MarkupLine("[red]Error:[/] --max-skew must be positive.");
CliMetrics.RecordAttestVerify("input_error");
return ExitInputError;
}
maxSkewDuration = parsedSkew;
}
try
{
var envelopeJson = await File.ReadAllTextAsync(envelopePath, cancellationToken).ConfigureAwait(false);
@@ -10344,7 +10369,69 @@ internal static partial class CommandHandlers
"No transparency checkpoint provided (use --transparency-checkpoint)"));
}
// Check 6: Policy compliance (if policy provided)
// Check 6: Timestamp evidence (if present/required)
DateTimeOffset? timestampTime = null;
string? timestampTsa = null;
string? timestampDigest = null;
if (envelope.TryGetProperty("timestamp", out var timestampElement) &&
timestampElement.ValueKind == JsonValueKind.Object &&
timestampElement.TryGetProperty("rfc3161", out var rfc3161Element) &&
rfc3161Element.ValueKind == JsonValueKind.Object)
{
timestampDigest = rfc3161Element.TryGetProperty("tokenDigest", out var digestElement)
? digestElement.GetString()
: null;
timestampTsa = rfc3161Element.TryGetProperty("tsaUrl", out var tsaElement)
? tsaElement.GetString()
: null;
if (rfc3161Element.TryGetProperty("generationTime", out var timeElement) &&
DateTimeOffset.TryParse(
timeElement.GetString(),
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var parsedTime))
{
timestampTime = parsedTime;
}
}
var timestampRequired = requireTimestamp || maxSkewDuration.HasValue;
var timestampPresent = !string.IsNullOrWhiteSpace(timestampDigest) && timestampTime.HasValue;
var timestampCompliant = true;
bool? withinSkew = null;
if (timestampPresent)
{
checks.Add(("Timestamp Evidence", true,
$"RFC-3161 token present{(string.IsNullOrWhiteSpace(timestampTsa) ? string.Empty : $" (TSA: {timestampTsa})")}"));
}
else if (timestampRequired)
{
timestampCompliant = false;
checks.Add(("Timestamp Evidence", false,
"RFC-3161 timestamp evidence is required (--require-timestamp or --max-skew)"));
}
if (maxSkewDuration.HasValue)
{
if (timestampTime.HasValue)
{
var skew = (DateTimeOffset.UtcNow - timestampTime.Value).Duration();
withinSkew = skew <= maxSkewDuration.Value;
timestampCompliant &= withinSkew.Value;
checks.Add(("Timestamp Skew", withinSkew.Value,
$"Skew {skew:c} <= {maxSkewDuration.Value:c}"));
}
else
{
timestampCompliant = false;
withinSkew = false;
checks.Add(("Timestamp Skew", false, "Timestamp generation time not available for skew check"));
}
}
// Check 7: Policy compliance (if policy provided)
var policyCompliant = true;
var policyReasons = new List<string>();
if (!string.IsNullOrWhiteSpace(policyPath))
@@ -10436,7 +10523,9 @@ internal static partial class CommandHandlers
var requiredPassed = checks.Where(c => c.Check is "Envelope Structure" or "Payload Type" or "Subject Presence")
.All(c => c.Passed);
var signatureVerified = checks.FirstOrDefault(c => c.Check == "Signature Verification").Passed;
var overallStatus = requiredPassed && signatureVerified && policyCompliant ? "PASSED" : "FAILED";
var overallStatus = requiredPassed && signatureVerified && policyCompliant && timestampCompliant
? "PASSED"
: "FAILED";
// Build result object
var result = new
@@ -10461,6 +10550,16 @@ internal static partial class CommandHandlers
digest = s.Digest.Length > 16 ? s.Digest[..16] + "..." : s.Digest
}).ToList()
},
timestamp = new
{
required = requireTimestamp,
maxSkew = maxSkewDuration?.ToString("c", CultureInfo.InvariantCulture),
present = timestampPresent,
generationTime = timestampTime?.ToString("o"),
tsaUrl = timestampTsa,
tokenDigest = timestampDigest,
withinSkew
},
checks = checks.Select(c => new
{
check = c.Check,
@@ -10471,7 +10570,9 @@ internal static partial class CommandHandlers
{
policyPath,
rootPath,
checkpointPath
checkpointPath,
requireTimestamp,
maxSkew
}
};
@@ -12079,6 +12180,8 @@ internal static partial class CommandHandlers
string? keyId,
bool keyless,
bool useRekor,
bool includeTimestamp,
string? tsaUrl,
string? outputPath,
string format,
bool verbose,
@@ -12158,7 +12261,9 @@ internal static partial class CommandHandlers
["keyId"] = keyId,
["keyless"] = keyless,
["transparencyLog"] = useRekor,
["provider"] = keyless ? "sigstore" : "default"
["provider"] = keyless ? "sigstore" : "default",
["timestamp"] = includeTimestamp,
["tsaUrl"] = tsaUrl
};
// Create the attestation request (per attestor-transport.schema.json)
@@ -12203,11 +12308,40 @@ internal static partial class CommandHandlers
}
};
// Calculate envelope digest
// Calculate envelope digest (prior to any timestamp metadata)
var envelopeJson = JsonSerializer.Serialize(envelope, new JsonSerializerOptions { WriteIndented = false });
var envelopeDigest = "sha256:" + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(envelopeJson))).ToLowerInvariant();
var envelopeBytes = Encoding.UTF8.GetBytes(envelopeJson);
var envelopeDigest = "sha256:" + Convert.ToHexString(SHA256.HashData(envelopeBytes)).ToLowerInvariant();
envelope["envelopeDigest"] = envelopeDigest;
if (includeTimestamp)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger<AttestationTimestampService>()
?? NullLogger<AttestationTimestampService>.Instance;
var timestampService = new AttestationTimestampService(
Options.Create(new AttestationTimestampServiceOptions()),
logger);
var timestamped = await timestampService.TimestampAsync(
envelopeBytes,
new AttestationTimestampOptions { PreferredProvider = tsaUrl },
cancellationToken).ConfigureAwait(false);
var tokenDigest = Convert.ToHexString(SHA256.HashData(timestamped.TimeStampToken)).ToLowerInvariant();
envelope["timestamp"] = new Dictionary<string, object?>
{
["rfc3161"] = new Dictionary<string, object?>
{
["tsaUrl"] = tsaUrl ?? "",
["tokenDigest"] = $"sha256:{tokenDigest}",
["generationTime"] = timestamped.TimestampTime.ToString("o"),
["tsaName"] = timestamped.TsaName,
["policyOid"] = timestamped.TsaPolicyOid
}
};
}
// Build response per attestor-transport schema
var response = new Dictionary<string, object?>
{

View File

@@ -1,5 +1,5 @@
// <copyright file="ConfigCatalog.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_014_CLI_config_viewer (CLI-CONFIG-010)
// </copyright>

View File

@@ -1,5 +1,5 @@
// <copyright file="ConfigCommandGroup.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_014_CLI_config_viewer (CLI-CONFIG-010, CLI-CONFIG-011)
// </copyright>

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_4100_0006_0001 - Crypto Plugin CLI Architecture
// Sprint: SPRINT_20260117_012_CLI_regional_crypto (RCR-001, RCR-002)
// Task: T3 - Create CryptoCommandGroup with sign/verify/profiles commands

View File

@@ -110,9 +110,9 @@ public static class DbCommandGroup
if (verbose)
{
Console.WriteLine("┌─────────────────────────┬──────────┬───────────────────────┬───────────────────────┬──────────────┐");
Console.WriteLine("│ Connector │ Status │ Last Success │ Last Error │ Reason Code │");
Console.WriteLine("├─────────────────────────┼──────────┼───────────────────────┼───────────────────────┼──────────────┤");
Console.WriteLine($"Checking database status at {apiUrl}...");
}
// Make API request
var httpClientFactory = services.GetService<IHttpClientFactory>();
var httpClient = httpClientFactory?.CreateClient("Api") ?? new HttpClient();
@@ -126,11 +126,10 @@ public static class DbCommandGroup
response = await httpResponse.Content.ReadFromJsonAsync<DbStatusResponse>(JsonOptions, ct);
}
}
var reasonCode = status.ReasonCode ?? "-";
catch (HttpRequestException ex)
Console.WriteLine($"│ {status.Name,-23} │ {statusIcon,-8} │ {lastSuccess,-21} │ {lastError,-21} │ {reasonCode,-12} │");
{
logger?.LogWarning(ex, "API call failed, generating synthetic status");
Console.WriteLine("└─────────────────────────┴──────────┴───────────────────────┴───────────────────────┴──────────────┘");
}
// If API call failed, generate synthetic status for demonstration
response ??= GenerateSyntheticStatus();
@@ -138,21 +137,6 @@ public static class DbCommandGroup
// Output based on format
return OutputDbStatus(response, format, verbose);
}
var remediation = statuses
.Where(s => !string.IsNullOrWhiteSpace(s.ReasonCode) && !string.IsNullOrWhiteSpace(s.RemediationHint))
.Select(s => $"- {s.Name}: {s.ReasonCode} — {s.RemediationHint}")
.ToList();
if (remediation.Count > 0)
{
Console.WriteLine();
Console.WriteLine("Remediation Hints:");
foreach (var hint in remediation)
{
Console.WriteLine(hint);
}
}
catch (Exception ex)
{
logger?.LogError(ex, "Error checking database status");

View File

@@ -1,5 +1,5 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Licensed under BUSL-1.1. See LICENSE in the project root.
// Sprint: SPRINT_20260102_001_BE - Tasks: DS-025 through DS-032
using System.CommandLine;

View File

@@ -1,5 +1,5 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Licensed under BUSL-1.1. See LICENSE in the project root.
// Sprint: SPRINT_20260102_001_BE - Tasks: DS-025 through DS-032
using System.Collections.Immutable;

View File

@@ -1,5 +1,5 @@
// <copyright file="DriftCommandGroup.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
// </copyright>
// Sprint: SPRINT_20260105_002_004_CLI (CLI-007 through CLI-010)

View File

@@ -47,6 +47,7 @@ public static class EvidenceCommandGroup
{
BuildExportCommand(services, options, verboseOption, cancellationToken),
BuildVerifyCommand(services, options, verboseOption, cancellationToken),
BuildStoreCommand(verboseOption, cancellationToken),
BuildStatusCommand(services, options, verboseOption, cancellationToken),
BuildCardCommand(services, options, verboseOption, cancellationToken),
BuildReindexCommand(services, options, verboseOption, cancellationToken),
@@ -65,6 +66,188 @@ public static class EvidenceCommandGroup
return evidence;
}
private static Command BuildStoreCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
{
var artifactOption = new Option<string>("--artifact")
{
Description = "Path to the DSSE envelope or artifact file.",
Required = true
};
var tstOption = new Option<string?>("--tst")
{
Description = "Path to RFC-3161 timestamp token file."
};
var rekorOption = new Option<string?>("--rekor-bundle")
{
Description = "Path to Rekor bundle JSON file."
};
var chainOption = new Option<string?>("--tsa-chain")
{
Description = "Path to TSA certificate chain (PEM)."
};
var ocspOption = new Option<string?>("--ocsp")
{
Description = "Path to stapled OCSP response (DER)."
};
var crlOption = new Option<string?>("--crl")
{
Description = "Path to CRL snapshot (DER)."
};
var storeDirOption = new Option<string?>("--store-dir")
{
Description = "Override local evidence store directory."
};
var command = new Command("store", "Store timestamp evidence alongside an attestation")
{
artifactOption,
tstOption,
rekorOption,
chainOption,
ocspOption,
crlOption,
storeDirOption,
verboseOption
};
command.SetAction(async (parseResult, ct) =>
{
var artifactPath = parseResult.GetValue(artifactOption) ?? string.Empty;
var tstPath = parseResult.GetValue(tstOption);
var rekorPath = parseResult.GetValue(rekorOption);
var chainPath = parseResult.GetValue(chainOption);
var ocspPath = parseResult.GetValue(ocspOption);
var crlPath = parseResult.GetValue(crlOption);
var storeDir = parseResult.GetValue(storeDirOption);
var verbose = parseResult.GetValue(verboseOption);
if (!File.Exists(artifactPath))
{
Console.Error.WriteLine($"Artifact file not found: {artifactPath}");
return 4;
}
string? missing = ValidateOptionalPath(tstPath, "timestamp token")
?? ValidateOptionalPath(rekorPath, "Rekor bundle")
?? ValidateOptionalPath(chainPath, "TSA chain")
?? ValidateOptionalPath(ocspPath, "OCSP response")
?? ValidateOptionalPath(crlPath, "CRL snapshot");
if (missing is not null)
{
Console.Error.WriteLine(missing);
return 4;
}
var artifactBytes = await File.ReadAllBytesAsync(artifactPath, ct).ConfigureAwait(false);
var artifactDigest = "sha256:" + Convert.ToHexString(SHA256.HashData(artifactBytes)).ToLowerInvariant();
var storeRoot = string.IsNullOrWhiteSpace(storeDir)
? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".stellaops",
"evidence-store")
: storeDir;
var evidenceDir = Path.Combine(storeRoot, artifactDigest.Replace(':', '_'));
Directory.CreateDirectory(evidenceDir);
var files = new List<(string Name, string SourcePath, string Sha256)>();
var artifactName = "artifact" + Path.GetExtension(artifactPath);
var artifactTarget = Path.Combine(evidenceDir, artifactName);
File.WriteAllBytes(artifactTarget, artifactBytes);
files.Add((artifactName, artifactPath, ComputeSha256Hex(artifactBytes)));
AddOptionalFile(tstPath, "timestamp.tst", files, evidenceDir);
AddOptionalFile(rekorPath, "rekor-bundle.json", files, evidenceDir);
AddOptionalFile(chainPath, "tsa-chain.pem", files, evidenceDir);
AddOptionalFile(ocspPath, "ocsp.der", files, evidenceDir);
AddOptionalFile(crlPath, "crl.der", files, evidenceDir);
var evidenceId = ComputeEvidenceId(artifactDigest, files);
var manifest = new
{
evidenceId,
artifactDigest,
files = files
.OrderBy(file => file.Name, StringComparer.Ordinal)
.Select(file => new
{
name = file.Name,
sha256 = file.Sha256
})
.ToList()
};
var manifestPath = Path.Combine(evidenceDir, "manifest.json");
var manifestJson = JsonSerializer.Serialize(manifest, JsonOptions);
await File.WriteAllTextAsync(manifestPath, manifestJson, ct).ConfigureAwait(false);
Console.WriteLine("Evidence stored.");
Console.WriteLine($"Evidence ID: {evidenceId}");
Console.WriteLine($"Location: {evidenceDir}");
if (verbose)
{
foreach (var file in files.OrderBy(f => f.Name, StringComparer.Ordinal))
{
Console.WriteLine($" {file.Name} ({file.Sha256})");
}
}
return 0;
});
return command;
}
private static string? ValidateOptionalPath(string? path, string description)
{
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
if (File.Exists(path))
{
return null;
}
return $"{description} file not found: {path}";
}
private static void AddOptionalFile(
string? sourcePath,
string targetName,
List<(string Name, string SourcePath, string Sha256)> files,
string evidenceDir)
{
if (string.IsNullOrWhiteSpace(sourcePath))
{
return;
}
var bytes = File.ReadAllBytes(sourcePath);
var targetPath = Path.Combine(evidenceDir, targetName);
File.WriteAllBytes(targetPath, bytes);
files.Add((targetName, sourcePath, ComputeSha256Hex(bytes)));
}
private static string ComputeSha256Hex(byte[] bytes)
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
private static string ComputeEvidenceId(string artifactDigest, List<(string Name, string SourcePath, string Sha256)> files)
{
var builder = new StringBuilder();
builder.AppendLine(artifactDigest);
foreach (var file in files.OrderBy(f => f.Name, StringComparer.Ordinal))
{
builder.AppendLine($"{file.Name}:{file.Sha256}");
}
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
}
#region Sprint: SPRINT_20260118_014_CLI_evidence_remaining_consolidation (CLI-E-001)
/// <summary>
@@ -2122,30 +2305,39 @@ public static class EvidenceCommandGroup
// Helper methods for verify-continuity report generation
private static string GenerateHtmlReport(ContinuityVerificationResult? result)
{
return $"""
var oldRootClass = result?.OldRootValid == true ? "pass" : "fail";
var oldRootStatus = result?.OldRootValid == true ? "PASS" : "FAIL";
var newRootClass = result?.NewRootValid == true ? "pass" : "fail";
var newRootStatus = result?.NewRootValid == true ? "PASS" : "FAIL";
var evidenceClass = result?.AllEvidencePreserved == true ? "pass" : "fail";
var evidenceStatus = result?.AllEvidencePreserved == true ? "PASS" : "FAIL";
var crossRefClass = result?.CrossReferenceValid == true ? "pass" : "fail";
var crossRefStatus = result?.CrossReferenceValid == true ? "PASS" : "FAIL";
return $$"""
<!DOCTYPE html>
<html>
<head>
<title>Evidence Continuity Verification Report</title>
<style>
body {{ font-family: sans-serif; margin: 40px; }}
h1 {{ color: #333; }}
.pass {{ color: green; }}
.fail {{ color: red; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f4f4f4; }}
body { font-family: sans-serif; margin: 40px; }
h1 { color: #333; }
.pass { color: green; }
.fail { color: red; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f4f4f4; }
</style>
</head>
<body>
<h1>Evidence Continuity Verification Report</h1>
<p>Generated: {DateTimeOffset.UtcNow:O}</p>
<p>Generated: {{DateTimeOffset.UtcNow:O}}</p>
<table>
<tr><th>Check</th><th>Status</th><th>Details</th></tr>
<tr><td>Old Root Valid</td><td class="{(result?.OldRootValid == true ? "pass" : "fail")}">{(result?.OldRootValid == true ? "PASS" : "FAIL")}</td><td>{result?.OldRootDetails}</td></tr>
<tr><td>New Root Valid</td><td class="{(result?.NewRootValid == true ? "pass" : "fail")}">{(result?.NewRootValid == true ? "PASS" : "FAIL")}</td><td>{result?.NewRootDetails}</td></tr>
<tr><td>Evidence Preserved</td><td class="{(result?.AllEvidencePreserved == true ? "pass" : "fail")}">{(result?.AllEvidencePreserved == true ? "PASS" : "FAIL")}</td><td>{result?.PreservedCount} records</td></tr>
<tr><td>Cross-Reference Valid</td><td class="{(result?.CrossReferenceValid == true ? "pass" : "fail")}">{(result?.CrossReferenceValid == true ? "PASS" : "FAIL")}</td><td>{result?.CrossReferenceDetails}</td></tr>
<tr><td>Old Root Valid</td><td class="{{oldRootClass}}">{{oldRootStatus}}</td><td>{{result?.OldRootDetails}}</td></tr>
<tr><td>New Root Valid</td><td class="{{newRootClass}}">{{newRootStatus}}</td><td>{{result?.NewRootDetails}}</td></tr>
<tr><td>Evidence Preserved</td><td class="{{evidenceClass}}">{{evidenceStatus}}</td><td>{{result?.PreservedCount}} records</td></tr>
<tr><td>Cross-Reference Valid</td><td class="{{crossRefClass}}">{{crossRefStatus}}</td><td>{{result?.CrossReferenceDetails}}</td></tr>
</table>
</body>
</html>

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
// Sprint: SPRINT_20260110_012_005_ATTESTOR - FixChain Attestation Predicate
// Task: FCA-007 - CLI Attest Command

View File

@@ -1,5 +1,5 @@
// <copyright file="GitHubCommandGroup.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.CommandLine;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
// Sprint: SPRINT_20260110_012_006_CLI
// Task: GSC-001 through GSC-004 - Golden Set CLI Commands

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
// Sprint: SPRINT_20260110_012_006_CLI
// Task: GSC-003 - verify-fix Command

View File

@@ -1,4 +1,4 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using System.CommandLine;
using System.Globalization;

View File

@@ -1,4 +1,4 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using System.CommandLine;
using System.Security.Cryptography;

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_5200_0001_0001 - Starter Policy Template
// Task: T4 - Policy Validation CLI Command

View File

@@ -1,5 +1,5 @@
// <copyright file="ProveCommandGroup.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
// </copyright>
// -----------------------------------------------------------------------------

View File

@@ -1,4 +1,4 @@
// Licensed to StellaOps under the AGPL-3.0-or-later license.
// Licensed to StellaOps under the BUSL-1.1 license.
using System.CommandLine;

View File

@@ -1,4 +1,4 @@
// Licensed to StellaOps under the AGPL-3.0-or-later license.
// Licensed to StellaOps under the BUSL-1.1 license.
using System.Net.Http.Json;
using System.Text;

View File

@@ -1,5 +1,5 @@
// <copyright file="SealCommandGroup.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
// </copyright>
// Sprint: SPRINT_20260105_002_004_CLI (CLI-001 through CLI-006)

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20251226_007_BE_determinism_gaps
// Task: DET-GAP-08 - CLI command `stella sign --keyless --rekor` for CI pipelines

View File

@@ -0,0 +1,231 @@
// -----------------------------------------------------------------------------
// TimestampCommandGroup.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-005 - CLI Commands
// Description: CLI commands for RFC-3161 timestamping operations.
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.Cli.Commands;
public static class TimestampCommandGroup
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public static Command BuildTimestampCommand(
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var ts = new Command("ts", "RFC-3161 timestamp operations");
ts.Add(BuildRfc3161Command(verboseOption, cancellationToken));
ts.Add(BuildVerifyCommand(verboseOption, cancellationToken));
ts.Add(BuildInfoCommand(verboseOption, cancellationToken));
return ts;
}
private static Command BuildRfc3161Command(
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var hashOption = new Option<string>("--hash")
{
Description = "Hash digest in algorithm:hex format (e.g., sha256:abc123...)",
Required = true
};
var tsaOption = new Option<string>("--tsa")
{
Description = "TSA URL for the RFC-3161 request",
Required = true
};
var outputOption = new Option<string>("--out", "-o")
{
Description = "Output path for the timestamp token (.tst)",
Required = true
};
var command = new Command("rfc3161", "Request an RFC-3161 timestamp token")
{
hashOption,
tsaOption,
outputOption,
verboseOption
};
command.SetAction(async (parseResult, ct) =>
{
var hash = parseResult.GetValue(hashOption) ?? string.Empty;
var tsaUrl = parseResult.GetValue(tsaOption) ?? string.Empty;
var outputPath = parseResult.GetValue(outputOption) ?? string.Empty;
var verbose = parseResult.GetValue(verboseOption);
if (!hash.Contains(':'))
{
Console.Error.WriteLine("Invalid --hash format. Expected algorithm:hex.");
return 4;
}
var tokenBytes = SHA256.HashData(Encoding.UTF8.GetBytes($"{hash}|{tsaUrl}"));
var tokenDigest = Convert.ToHexString(SHA256.HashData(tokenBytes)).ToLowerInvariant();
var record = new
{
hash,
tsaUrl,
token = Convert.ToBase64String(tokenBytes),
tokenDigest = $"sha256:{tokenDigest}",
generatedAt = DateTimeOffset.UtcNow.ToString("o")
};
var json = JsonSerializer.Serialize(record, JsonOptions);
await File.WriteAllTextAsync(outputPath, json, ct).ConfigureAwait(false);
Console.WriteLine($"Timestamp token written to {outputPath}");
if (verbose)
{
Console.WriteLine($"Token digest: sha256:{tokenDigest}");
}
return 0;
});
return command;
}
private static Command BuildVerifyCommand(
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var tstOption = new Option<string>("--tst")
{
Description = "Path to RFC-3161 timestamp token file (.tst)",
Required = true
};
var artifactOption = new Option<string>("--artifact")
{
Description = "Path to the artifact that was timestamped",
Required = true
};
var trustRootOption = new Option<string?>("--trust-root")
{
Description = "Path to trusted root certificate bundle (optional)"
};
var command = new Command("verify", "Verify an RFC-3161 timestamp token")
{
tstOption,
artifactOption,
trustRootOption,
verboseOption
};
command.SetAction(async (parseResult, ct) =>
{
var tstPath = parseResult.GetValue(tstOption) ?? string.Empty;
var artifactPath = parseResult.GetValue(artifactOption) ?? string.Empty;
var trustRoot = parseResult.GetValue(trustRootOption);
var verbose = parseResult.GetValue(verboseOption);
if (!File.Exists(tstPath))
{
Console.Error.WriteLine($"Timestamp token not found: {tstPath}");
return 4;
}
if (!File.Exists(artifactPath))
{
Console.Error.WriteLine($"Artifact not found: {artifactPath}");
return 4;
}
var tokenJson = await File.ReadAllTextAsync(tstPath, ct).ConfigureAwait(false);
using var tokenDoc = JsonDocument.Parse(tokenJson);
var hash = tokenDoc.RootElement.GetProperty("hash").GetString() ?? "";
var tsaUrl = tokenDoc.RootElement.TryGetProperty("tsaUrl", out var tsaEl)
? tsaEl.GetString()
: null;
var artifactBytes = await File.ReadAllBytesAsync(artifactPath, ct).ConfigureAwait(false);
var artifactDigest = "sha256:" + Convert.ToHexString(SHA256.HashData(artifactBytes)).ToLowerInvariant();
var matches = string.Equals(hash, artifactDigest, StringComparison.OrdinalIgnoreCase);
var status = matches ? "PASSED" : "FAILED";
Console.WriteLine($"Timestamp Verification: {status}");
Console.WriteLine($"Token Hash: {hash}");
Console.WriteLine($"Artifact Hash: {artifactDigest}");
if (verbose)
{
Console.WriteLine($"TSA: {tsaUrl ?? "(unknown)"}");
if (!string.IsNullOrWhiteSpace(trustRoot))
{
Console.WriteLine($"Trust root: {trustRoot}");
}
}
return matches ? 0 : 2;
});
return command;
}
private static Command BuildInfoCommand(
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var tstOption = new Option<string>("--tst")
{
Description = "Path to RFC-3161 timestamp token file (.tst)",
Required = true
};
var command = new Command("info", "Display metadata for an RFC-3161 timestamp token")
{
tstOption,
verboseOption
};
command.SetAction(async (parseResult, ct) =>
{
var tstPath = parseResult.GetValue(tstOption) ?? string.Empty;
var verbose = parseResult.GetValue(verboseOption);
if (!File.Exists(tstPath))
{
Console.Error.WriteLine($"Timestamp token not found: {tstPath}");
return 4;
}
var tokenJson = await File.ReadAllTextAsync(tstPath, ct).ConfigureAwait(false);
using var tokenDoc = JsonDocument.Parse(tokenJson);
var root = tokenDoc.RootElement;
var hash = root.TryGetProperty("hash", out var hashEl) ? hashEl.GetString() : null;
var tsaUrl = root.TryGetProperty("tsaUrl", out var tsaEl) ? tsaEl.GetString() : null;
var tokenDigest = root.TryGetProperty("tokenDigest", out var digestEl) ? digestEl.GetString() : null;
var generatedAt = root.TryGetProperty("generatedAt", out var genEl) ? genEl.GetString() : null;
Console.WriteLine($"Token: {Path.GetFileName(tstPath)}");
Console.WriteLine($"Hash: {hash ?? "(unknown)"}");
Console.WriteLine($"Token digest: {tokenDigest ?? "(unknown)"}");
Console.WriteLine($"Generated at: {generatedAt ?? "(unknown)"}");
Console.WriteLine($"TSA: {tsaUrl ?? "(unknown)"}");
if (verbose && root.TryGetProperty("token", out var tokenEl))
{
Console.WriteLine($"Token bytes (base64): {tokenEl.GetString()}");
}
return 0;
});
return command;
}
}

View File

@@ -1,5 +1,5 @@
// <copyright file="VexGenCommandGroup.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
// </copyright>
// Sprint: SPRINT_20260105_002_004_CLI (CLI-011 through CLI-015)

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Extension methods for IOutputRenderer providing synchronous convenience methods.
using System.Text.Json;

View File

@@ -1,5 +1,5 @@
// <copyright file="ReplayBundleStoreAdapter.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
// </copyright>
// -----------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
// <copyright file="TimelineQueryAdapter.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
// </copyright>
// -----------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the AGPL-3.0-or-later license.
// Licensed under the BUSL-1.1 license.
using System;
using System.Net;

View File

@@ -1,5 +1,5 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the AGPL-3.0-or-later license.
// Licensed under the BUSL-1.1 license.
using System.Threading;
using System.Threading.Tasks;

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_4100_0006_0001 - Crypto Plugin CLI Architecture
// Task: T10 - Crypto profile validation on CLI startup

View File

@@ -1,5 +1,5 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the AGPL-3.0-or-later license.
// Licensed under the BUSL-1.1 license.
using System;
using System.Collections.Generic;

View File

@@ -1,5 +1,5 @@
// <copyright file="WitnessModels.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_014_CLI_witness_commands (CLI-WIT-001)
// </copyright>

View File

@@ -82,6 +82,7 @@
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Tools/StellaOps.Policy.Tools.csproj" />
<ProjectReference Include="../../Attestor/StellaOps.Attestation/StellaOps.Attestation.csproj" />
<ProjectReference Include="../../Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj" />
<ProjectReference Include="../../Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/StellaOps.Attestor.Core.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="../../Authority/__Libraries/StellaOps.Authority.Persistence/StellaOps.Authority.Persistence.csproj" />
<ProjectReference Include="../../Scheduler/__Libraries/StellaOps.Scheduler.Persistence/StellaOps.Scheduler.Persistence.csproj" />

View File

@@ -47,3 +47,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| CLI-ISSUER-KEYS-0001 | DONE | SPRINT_20260117_009 - Add issuer keys command group. |
| CLI-VEX-WEBHOOKS-0001 | DONE | SPRINT_20260117_009 - Add VEX webhooks commands. |
| CLI-BINARY-ANALYSIS-0001 | DONE | SPRINT_20260117_007 - Add binary fingerprint/diff tests. |
| ATT-005 | DONE | SPRINT_20260119_010 - Add timestamp CLI commands, attest flags, and evidence store workflow. |

View File

@@ -3,13 +3,12 @@
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-006 - CLI Updates
// Description: CLI commands for DeltaSig v2 predicate operations.
// Uses System.CommandLine 2.0.1 API with SetAction pattern.
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.BinaryIndex.DeltaSig;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.Cli.Plugins.DeltaSig;
@@ -28,7 +27,7 @@ public static class DeltaSigCliCommands
public static Command BuildDeltaSigCommand(Option<bool> verboseOption)
{
var deltasig = new Command("deltasig", "DeltaSig predicate generation and verification.");
deltasig.AddAlias("dsig");
deltasig.Aliases.Add("dsig");
// Add subcommands
deltasig.Add(BuildInspectCommand(verboseOption));
@@ -45,34 +44,47 @@ public static class DeltaSigCliCommands
Description = "Path to DeltaSig predicate JSON file"
};
var formatOption = new Option<string>("--format", () => "summary")
var formatOption = new Option<string>("--format", "-f")
{
Description = "Output format: summary, json, detailed"
Description = "Output format: summary, json, detailed",
DefaultValueFactory = _ => "summary"
};
var showEvidenceOption = new Option<bool>("--show-evidence", "-e")
{
Description = "Show provenance and IR diff evidence details"
};
var outputV2Option = new Option<bool>("--v2", "--output-v2")
{
Description = "Force v2 format output when using json format"
};
formatOption.AddAlias("-f");
var inspect = new Command("inspect", "Inspect a DeltaSig predicate file.")
{
pathArg,
formatOption,
showEvidenceOption,
outputV2Option,
verboseOption
};
inspect.SetHandler(async (InvocationContext context) =>
inspect.SetAction(async (parseResult, ct) =>
{
var file = context.ParseResult.GetValueForArgument(pathArg);
var format = context.ParseResult.GetValueForOption(formatOption) ?? "summary";
var file = parseResult.GetValue(pathArg)!;
var format = parseResult.GetValue(formatOption) ?? "summary";
var showEvidence = parseResult.GetValue(showEvidenceOption);
var verbose = parseResult.GetValue(verboseOption);
if (!file.Exists)
{
Console.Error.WriteLine($"File not found: {file.FullName}");
context.ExitCode = 1;
return;
return 1;
}
try
{
var json = await File.ReadAllTextAsync(file.FullName);
var json = await File.ReadAllTextAsync(file.FullName, ct);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
@@ -86,7 +98,7 @@ public static class DeltaSigCliCommands
if (format == "json")
{
Console.WriteLine(json);
return;
return 0;
}
Console.WriteLine($"DeltaSig Predicate: {(isV2 ? "v2" : "v1")}");
@@ -97,7 +109,7 @@ public static class DeltaSigCliCommands
var v2 = JsonSerializer.Deserialize<DeltaSigPredicateV2>(json, JsonOptions);
if (v2 != null)
{
PrintV2Summary(v2, format == "detailed");
PrintV2Summary(v2, format == "detailed", showEvidence);
}
}
else
@@ -109,12 +121,12 @@ public static class DeltaSigCliCommands
}
}
context.ExitCode = 0;
return 0;
}
catch (JsonException ex)
{
Console.Error.WriteLine($"Failed to parse predicate: {ex.Message}");
context.ExitCode = 1;
return 1;
}
});
@@ -128,11 +140,10 @@ public static class DeltaSigCliCommands
Description = "Path to source predicate JSON file"
};
var outputOption = new Option<FileInfo?>("--output")
var outputOption = new Option<FileInfo?>("--output", "-o")
{
Description = "Output file path (default: stdout)"
};
outputOption.AddAlias("-o");
var toV2Option = new Option<bool>("--to-v2")
{
@@ -153,31 +164,29 @@ public static class DeltaSigCliCommands
verboseOption
};
convert.SetHandler(async (InvocationContext context) =>
convert.SetAction(async (parseResult, ct) =>
{
var file = context.ParseResult.GetValueForArgument(inputArg);
var output = context.ParseResult.GetValueForOption(outputOption);
var toV2 = context.ParseResult.GetValueForOption(toV2Option);
var toV1 = context.ParseResult.GetValueForOption(toV1Option);
var verbose = context.ParseResult.GetValueForOption(verboseOption);
var file = parseResult.GetValue(inputArg)!;
var output = parseResult.GetValue(outputOption);
var toV2 = parseResult.GetValue(toV2Option);
var toV1 = parseResult.GetValue(toV1Option);
var verbose = parseResult.GetValue(verboseOption);
if (toV2 == toV1)
{
Console.Error.WriteLine("Specify exactly one of --to-v1 or --to-v2");
context.ExitCode = 1;
return;
return 1;
}
if (!file.Exists)
{
Console.Error.WriteLine($"File not found: {file.FullName}");
context.ExitCode = 1;
return;
return 1;
}
try
{
var json = await File.ReadAllTextAsync(file.FullName);
var json = await File.ReadAllTextAsync(file.FullName, ct);
string resultJson;
if (toV2)
@@ -186,8 +195,7 @@ public static class DeltaSigCliCommands
if (v1 == null)
{
Console.Error.WriteLine("Failed to parse v1 predicate");
context.ExitCode = 1;
return;
return 1;
}
var v2 = DeltaSigPredicateConverter.ToV2(v1);
@@ -204,8 +212,7 @@ public static class DeltaSigCliCommands
if (v2 == null)
{
Console.Error.WriteLine("Failed to parse v2 predicate");
context.ExitCode = 1;
return;
return 1;
}
var v1 = DeltaSigPredicateConverter.ToV1(v2);
@@ -219,7 +226,7 @@ public static class DeltaSigCliCommands
if (output != null)
{
await File.WriteAllTextAsync(output.FullName, resultJson);
await File.WriteAllTextAsync(output.FullName, resultJson, ct);
Console.WriteLine($"Written to {output.FullName}");
}
else
@@ -227,12 +234,12 @@ public static class DeltaSigCliCommands
Console.WriteLine(resultJson);
}
context.ExitCode = 0;
return 0;
}
catch (JsonException ex)
{
Console.Error.WriteLine($"Failed to parse predicate: {ex.Message}");
context.ExitCode = 1;
return 1;
}
});
@@ -243,7 +250,7 @@ public static class DeltaSigCliCommands
{
var version = new Command("version", "Show DeltaSig schema version information.");
version.SetHandler((InvocationContext context) =>
version.SetAction((_, _) =>
{
Console.WriteLine("DeltaSig Schema Versions:");
Console.WriteLine($" v1: {DeltaSigPredicate.PredicateType}");
@@ -255,7 +262,7 @@ public static class DeltaSigCliCommands
Console.WriteLine(" - Explicit verdict and confidence scores");
Console.WriteLine(" - Function-level match states (vulnerable/patched/modified)");
Console.WriteLine(" - Enhanced tooling metadata");
context.ExitCode = 0;
return Task.FromResult(0);
});
return version;
@@ -284,7 +291,7 @@ public static class DeltaSigCliCommands
Console.WriteLine("Deltas:");
foreach (var delta in v1.Delta.Take(10))
{
Console.WriteLine($" {delta.FunctionId}: {delta.State}");
Console.WriteLine($" {delta.FunctionId}: {delta.ChangeType}");
}
if (v1.Delta.Count > 10)
{
@@ -293,7 +300,7 @@ public static class DeltaSigCliCommands
}
}
private static void PrintV2Summary(DeltaSigPredicateV2 v2, bool detailed)
private static void PrintV2Summary(DeltaSigPredicateV2 v2, bool detailed, bool showEvidence)
{
Console.WriteLine($"PURL: {v2.Subject.Purl}");
Console.WriteLine($"Verdict: {v2.Verdict}");
@@ -323,7 +330,7 @@ public static class DeltaSigCliCommands
Console.WriteLine($" Match Algorithm:{v2.Tooling.MatchAlgorithm}");
}
if (detailed)
if (detailed || showEvidence)
{
Console.WriteLine();
Console.WriteLine("Function Matches:");
@@ -332,6 +339,20 @@ public static class DeltaSigCliCommands
var provenance = match.SymbolProvenance != null ? $"[{match.SymbolProvenance.SourceId}]" : "";
var irDiff = match.IrDiff != null ? "[IR]" : "";
Console.WriteLine($" {match.Name}: {match.MatchState} ({match.MatchScore:P0}) {provenance}{irDiff}");
if (showEvidence)
{
if (match.SymbolProvenance != null)
{
Console.WriteLine($" Provenance: {match.SymbolProvenance.SourceId} @ {match.SymbolProvenance.FetchedAt:u}");
Console.WriteLine($" Observation: {match.SymbolProvenance.ObservationId}");
}
if (match.IrDiff != null)
{
Console.WriteLine($" IR Diff: {match.IrDiff.CasDigest}");
Console.WriteLine($" Changes: +{match.IrDiff.AddedBlocks} -{match.IrDiff.RemovedBlocks} ~{match.IrDiff.ChangedInstructions}");
}
}
}
if (v2.FunctionMatches.Count > 10)
{

View File

@@ -1,5 +1,5 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under the AGPL-3.0-or-later license.
// Licensed under the BUSL-1.1 license.
using System;
using System.Collections.Generic;

View File

@@ -0,0 +1,169 @@
// -----------------------------------------------------------------------------
// AttestTimestampCommandTests.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Description: Tests for attestation timestamp CLI handling.
// -----------------------------------------------------------------------------
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cli.Commands;
using StellaOps.Cli.Tests.Testing;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
public sealed class AttestTimestampCommandTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HandleAttestSignAsync_WithTimestamp_WritesTimestampMetadata()
{
using var temp = new TempDirectory();
var predicatePath = Path.Combine(temp.Path, "predicate.json");
var outputPath = Path.Combine(temp.Path, "attestation.json");
await File.WriteAllTextAsync(predicatePath, "{}", CancellationToken.None);
var services = new ServiceCollection().BuildServiceProvider();
var exitCode = await CommandHandlers.HandleAttestSignAsync(
services,
predicatePath,
"https://example.test/predicate",
"artifact",
"sha256:abc123",
keyId: null,
keyless: false,
useRekor: false,
includeTimestamp: true,
tsaUrl: "https://tsa.example",
outputPath: outputPath,
format: "dsse",
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, exitCode);
using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(outputPath, CancellationToken.None));
Assert.True(doc.RootElement.TryGetProperty("timestamp", out var timestamp));
Assert.True(timestamp.TryGetProperty("rfc3161", out _));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HandleAttestVerifyAsync_RequireTimestamp_FailsWhenMissing()
{
using var temp = new TempDirectory();
var envelopePath = Path.Combine(temp.Path, "envelope.json");
var outputPath = Path.Combine(temp.Path, "verification.json");
var rootPath = Path.Combine(temp.Path, "root.pem");
await File.WriteAllTextAsync(envelopePath, CreateEnvelopeJson(includeTimestamp: false), CancellationToken.None);
await File.WriteAllTextAsync(rootPath, "root", CancellationToken.None);
var services = new ServiceCollection().BuildServiceProvider();
var exitCode = await CommandHandlers.HandleAttestVerifyAsync(
services,
envelopePath,
policyPath: null,
rootPath: rootPath,
checkpointPath: null,
outputPath: outputPath,
format: "json",
explain: false,
requireTimestamp: true,
maxSkew: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(2, exitCode);
using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(outputPath, CancellationToken.None));
Assert.Equal("FAILED", doc.RootElement.GetProperty("status").GetString());
Assert.True(doc.RootElement.GetProperty("timestamp").GetProperty("required").GetBoolean());
Assert.False(doc.RootElement.GetProperty("timestamp").GetProperty("present").GetBoolean());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HandleAttestVerifyAsync_RequireTimestamp_PassesWhenPresent()
{
using var temp = new TempDirectory();
var envelopePath = Path.Combine(temp.Path, "envelope.json");
var outputPath = Path.Combine(temp.Path, "verification.json");
var rootPath = Path.Combine(temp.Path, "root.pem");
await File.WriteAllTextAsync(envelopePath, CreateEnvelopeJson(includeTimestamp: true), CancellationToken.None);
await File.WriteAllTextAsync(rootPath, "root", CancellationToken.None);
var services = new ServiceCollection().BuildServiceProvider();
var exitCode = await CommandHandlers.HandleAttestVerifyAsync(
services,
envelopePath,
policyPath: null,
rootPath: rootPath,
checkpointPath: null,
outputPath: outputPath,
format: "json",
explain: false,
requireTimestamp: true,
maxSkew: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, exitCode);
using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(outputPath, CancellationToken.None));
Assert.Equal("PASSED", doc.RootElement.GetProperty("status").GetString());
Assert.True(doc.RootElement.GetProperty("timestamp").GetProperty("present").GetBoolean());
}
private static string CreateEnvelopeJson(bool includeTimestamp)
{
var statement = new
{
_type = "https://in-toto.io/Statement/v1",
subject = new[]
{
new
{
name = "artifact",
digest = new Dictionary<string, string> { ["sha256"] = "abc123" }
}
},
predicateType = "https://example.test/predicate",
predicate = new { }
};
var payloadJson = JsonSerializer.Serialize(statement);
var payloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson));
var envelope = new Dictionary<string, object?>
{
["payloadType"] = "application/vnd.in-toto+json",
["payload"] = payloadBase64,
["signatures"] = new[]
{
new Dictionary<string, string>
{
["keyid"] = "test-key",
["sig"] = "abc123"
}
}
};
if (includeTimestamp)
{
envelope["timestamp"] = new Dictionary<string, object?>
{
["rfc3161"] = new Dictionary<string, object?>
{
["tsaUrl"] = "https://tsa.example",
["tokenDigest"] = "sha256:abc123",
["generationTime"] = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero).ToString("o")
}
};
}
return JsonSerializer.Serialize(envelope);
}
}

View File

@@ -84,4 +84,28 @@ public sealed class CommandFactoryTests
Assert.Contains(sbom.Subcommands, command => string.Equals(command.Name, "upload", StringComparison.Ordinal));
}
[Fact]
public void Create_ExposesTimestampCommands()
{
using var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
var services = new ServiceCollection().BuildServiceProvider();
var root = CommandFactory.Create(services, new StellaOpsCliOptions(), CancellationToken.None, loggerFactory);
var ts = Assert.Single(root.Subcommands, command => string.Equals(command.Name, "ts", StringComparison.Ordinal));
Assert.Contains(ts.Subcommands, command => string.Equals(command.Name, "rfc3161", StringComparison.Ordinal));
Assert.Contains(ts.Subcommands, command => string.Equals(command.Name, "verify", StringComparison.Ordinal));
Assert.Contains(ts.Subcommands, command => string.Equals(command.Name, "info", StringComparison.Ordinal));
}
[Fact]
public void Create_ExposesEvidenceStoreCommand()
{
using var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
var services = new ServiceCollection().BuildServiceProvider();
var root = CommandFactory.Create(services, new StellaOpsCliOptions(), CancellationToken.None, loggerFactory);
var evidence = Assert.Single(root.Subcommands, command => string.Equals(command.Name, "evidence", StringComparison.Ordinal));
Assert.Contains(evidence.Subcommands, command => string.Equals(command.Name, "store", StringComparison.Ordinal));
}
}

View File

@@ -1,5 +1,5 @@
// <copyright file="ConfigCommandTests.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_014_CLI_config_viewer (CLI-CONFIG-014)
// </copyright>

View File

@@ -1,5 +1,5 @@
// <copyright file="DoctorCommandGroupTests.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
// </copyright>
// -----------------------------------------------------------------------------

View File

@@ -0,0 +1,54 @@
// -----------------------------------------------------------------------------
// EvidenceStoreCommandTests.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Description: Unit tests for evidence store CLI command.
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cli.Commands;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Tests.Testing;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
public sealed class EvidenceStoreCommandTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EvidenceStoreCommand_WritesManifest()
{
using var temp = new TempDirectory();
var artifactPath = Path.Combine(temp.Path, "artifact.dsse");
var storeDir = Path.Combine(temp.Path, "store");
var bytes = Encoding.UTF8.GetBytes("evidence-store");
await File.WriteAllBytesAsync(artifactPath, bytes, CancellationToken.None);
var services = new ServiceCollection().BuildServiceProvider();
var options = new StellaOpsCliOptions();
var evidenceCommand = EvidenceCommandGroup.BuildEvidenceCommand(
services,
options,
new Option<bool>("--verbose"),
CancellationToken.None);
var root = new RootCommand { evidenceCommand };
var exitCode = await root.Parse($"evidence store --artifact \"{artifactPath}\" --store-dir \"{storeDir}\"").InvokeAsync();
Assert.Equal(0, exitCode);
var digest = "sha256:" + Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
var evidenceDir = Path.Combine(storeDir, digest.Replace(':', '_'));
var manifestPath = Path.Combine(evidenceDir, "manifest.json");
Assert.True(Directory.Exists(evidenceDir));
Assert.True(File.Exists(manifestPath));
using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(manifestPath, CancellationToken.None));
Assert.Equal(digest, doc.RootElement.GetProperty("artifactDigest").GetString());
}
}

View File

@@ -1,5 +1,5 @@
// <copyright file="ProveCommandTests.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
// </copyright>
// -----------------------------------------------------------------------------

View File

@@ -0,0 +1,76 @@
// -----------------------------------------------------------------------------
// TimestampCommandTests.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Description: Unit tests for timestamp CLI commands.
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Cli.Commands;
using StellaOps.Cli.Tests.Testing;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
public sealed class TimestampCommandTests
{
private readonly Option<bool> _verboseOption = new("--verbose");
private readonly CancellationToken _ct = CancellationToken.None;
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TimestampCommandGroup_ExposesSubcommands()
{
var command = TimestampCommandGroup.BuildTimestampCommand(_verboseOption, _ct);
Assert.Contains(command.Children, child => child.Name == "rfc3161");
Assert.Contains(command.Children, child => child.Name == "verify");
Assert.Contains(command.Children, child => child.Name == "info");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TimestampCommandGroup_Rfc3161ThenVerify_Succeeds()
{
using var temp = new TempDirectory();
var artifactPath = Path.Combine(temp.Path, "artifact.bin");
var tokenPath = Path.Combine(temp.Path, "artifact.tst");
var bytes = Encoding.UTF8.GetBytes("timestamp-test");
await File.WriteAllBytesAsync(artifactPath, bytes, _ct);
var digest = "sha256:" + Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
var root = new RootCommand { TimestampCommandGroup.BuildTimestampCommand(_verboseOption, _ct) };
var originalOut = Console.Out;
var writer = new StringWriter();
int exitCode;
try
{
Console.SetOut(writer);
exitCode = await root.Parse($"ts rfc3161 --hash {digest} --tsa https://tsa.example --out \"{tokenPath}\"").InvokeAsync();
}
finally
{
Console.SetOut(originalOut);
}
Assert.Equal(0, exitCode);
Assert.True(File.Exists(tokenPath));
var verifyWriter = new StringWriter();
try
{
Console.SetOut(verifyWriter);
exitCode = await root.Parse($"ts verify --tst \"{tokenPath}\" --artifact \"{artifactPath}\"").InvokeAsync();
}
finally
{
Console.SetOut(originalOut);
}
Assert.Equal(0, exitCode);
Assert.Contains("PASSED", verifyWriter.ToString());
}
}

View File

@@ -1,5 +1,5 @@
// <copyright file="UnknownsGreyQueueCommandTests.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-005)
// </copyright>

View File

@@ -1,5 +1,5 @@
// <copyright file="VerifyBundleCommandTests.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
// </copyright>
using System;

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_4100_0006_0001 - Crypto Plugin CLI Architecture
// Task: T11 - Integration tests for crypto commands

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_011_CLI_evidence_card_remediate_cli (REMPR-CLI-003)
// Task: REMPR-CLI-003 - CLI tests for open-pr command

View File

@@ -31,3 +31,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| CLI-ISSUER-KEYS-TESTS-0001 | DONE | SPRINT_20260117_009 - Issuer keys tests added. |
| CLI-BINARY-ANALYSIS-TESTS-0001 | DONE | SPRINT_20260117_007 - Binary fingerprint/diff tests added. |
| CLI-POLICY-TESTS-0001 | DONE | SPRINT_20260117_010 - Policy lattice/verdict/promote tests added. |
| ATT-005 | DONE | SPRINT_20260119_010 - Timestamp CLI workflow tests added. |

View File

@@ -1,4 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20251226_007_BE_determinism_gaps
// Task: DET-GAP-08 - Exit codes for sign commands