test fixes and new product advisories work

This commit is contained in:
master
2026-01-28 02:30:48 +02:00
parent 82caceba56
commit 644887997c
288 changed files with 69101 additions and 375 deletions

View File

@@ -39,6 +39,7 @@ public static class SignalsCommandGroup
signalsCommand.Add(BuildInspectCommand(services, verboseOption, cancellationToken));
signalsCommand.Add(BuildListCommand(services, verboseOption, cancellationToken));
signalsCommand.Add(BuildSummaryCommand(services, verboseOption, cancellationToken));
signalsCommand.Add(BuildVerifyChainCommand(services, verboseOption, cancellationToken));
return signalsCommand;
}
@@ -304,6 +305,252 @@ public static class SignalsCommandGroup
#endregion
#region Verify Chain Command (SIGNING-002)
/// <summary>
/// Build the 'signals verify-chain' command.
/// Sprint: SPRINT_0127_0002_Signals_ebpf_syscall_reachability_proofs (SIGNING-002)
/// Verifies integrity of signed runtime evidence chain.
/// </summary>
private static Command BuildVerifyChainCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var pathArg = new Argument<string>("path")
{
Description = "Path to evidence directory containing signed chunks"
};
var offlineOption = new Option<bool>("--offline")
{
Description = "Offline mode - skip Rekor verification"
};
var reportOption = new Option<string?>("--report", "-r")
{
Description = "Output path for JSON verification report"
};
var formatOption = new Option<string>("--format", "-f")
{
Description = "Output format: text (default), json"
};
formatOption.SetDefaultValue("text");
var verifyChainCommand = new Command("verify-chain", "Verify integrity of signed runtime evidence chain")
{
pathArg,
offlineOption,
reportOption,
formatOption,
verboseOption
};
verifyChainCommand.SetAction(async (parseResult, ct) =>
{
var path = parseResult.GetValue(pathArg) ?? string.Empty;
var offline = parseResult.GetValue(offlineOption);
var reportPath = parseResult.GetValue(reportOption);
var format = parseResult.GetValue(formatOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
if (!Directory.Exists(path))
{
Console.Error.WriteLine($"Error: Directory not found: {path}");
return 1;
}
// Find signed chunk files (look for .dsse.json sidecar files)
var dsseFiles = Directory.GetFiles(path, "*.dsse.json", SearchOption.TopDirectoryOnly)
.OrderBy(f => f)
.ToList();
// Also look for chain state file
var chainStateFiles = Directory.GetFiles(path, "chain-*.json", SearchOption.TopDirectoryOnly);
if (dsseFiles.Count == 0)
{
Console.Error.WriteLine($"Error: No signed chunks found in: {path}");
Console.Error.WriteLine("Looking for: *.dsse.json files");
return 1;
}
var report = new ChainVerificationReport
{
Path = path,
VerifiedAt = DateTimeOffset.UtcNow,
OfflineMode = offline,
TotalChunks = dsseFiles.Count,
ChunkResults = []
};
if (!format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Evidence Chain Verification");
Console.WriteLine("===========================");
Console.WriteLine();
Console.WriteLine($"Path: {path}");
Console.WriteLine($"Chunks: {dsseFiles.Count}");
Console.WriteLine($"Mode: {(offline ? "Offline" : "Online")}");
Console.WriteLine();
}
string? expectedPreviousHash = null;
int expectedSequence = -1;
DateTimeOffset? previousEndTime = null;
int passedCount = 0;
int failedCount = 0;
foreach (var dsseFile in dsseFiles)
{
var chunkResult = new ChunkVerificationResult
{
FilePath = dsseFile,
Errors = []
};
try
{
var dsseJson = await File.ReadAllTextAsync(dsseFile, ct);
var envelope = JsonSerializer.Deserialize<DsseEnvelopeInfo>(dsseJson, JsonOptions);
if (envelope == null)
{
chunkResult.Errors.Add("Failed to parse DSSE envelope");
report.ChunkResults.Add(chunkResult);
failedCount++;
continue;
}
// Decode payload to get predicate
var payloadJson = System.Text.Encoding.UTF8.GetString(
Convert.FromBase64String(envelope.Payload));
var statement = JsonSerializer.Deserialize<InTotoStatementInfo>(payloadJson, JsonOptions);
if (statement?.Predicate == null)
{
chunkResult.Errors.Add("Failed to parse in-toto statement");
report.ChunkResults.Add(chunkResult);
failedCount++;
continue;
}
var predicate = statement.Predicate;
chunkResult.ChunkId = predicate.ChunkId;
chunkResult.ChunkSequence = predicate.ChunkSequence;
chunkResult.EventCount = predicate.EventCount;
chunkResult.TimeRange = new TimeRangeInfo
{
Start = predicate.TimeRange?.Start,
End = predicate.TimeRange?.End
};
// Initialize expected sequence from first chunk
if (expectedSequence < 0)
{
expectedSequence = predicate.ChunkSequence;
}
// Verify chain linkage
if (expectedPreviousHash != null && predicate.PreviousChunkId != expectedPreviousHash)
{
chunkResult.Errors.Add($"Chain broken: expected previous_chunk_id={expectedPreviousHash}, got={predicate.PreviousChunkId}");
}
// Verify sequence continuity
if (predicate.ChunkSequence != expectedSequence)
{
chunkResult.Errors.Add($"Sequence gap: expected={expectedSequence}, got={predicate.ChunkSequence}");
}
// Verify time monotonicity
if (previousEndTime.HasValue && predicate.TimeRange?.Start < previousEndTime)
{
chunkResult.Errors.Add($"Time overlap: chunk starts at {predicate.TimeRange?.Start}, but previous ended at {previousEndTime}");
}
// Verify signature is present
if (envelope.Signatures == null || envelope.Signatures.Count == 0)
{
chunkResult.Errors.Add("No signatures found in envelope");
}
// Note: Full cryptographic verification would require the signing keys
// In offline mode, we only verify structural integrity
chunkResult.Passed = chunkResult.Errors.Count == 0;
if (chunkResult.Passed)
{
passedCount++;
}
else
{
failedCount++;
}
// Update expectations for next chunk
expectedPreviousHash = predicate.ChunkId;
expectedSequence++;
previousEndTime = predicate.TimeRange?.End;
}
catch (Exception ex)
{
chunkResult.Errors.Add($"Exception: {ex.Message}");
failedCount++;
}
report.ChunkResults.Add(chunkResult);
if (verbose && !format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
var status = chunkResult.Passed ? "✓" : "✗";
Console.WriteLine($" {status} {Path.GetFileName(dsseFile)}: seq={chunkResult.ChunkSequence}, events={chunkResult.EventCount}");
foreach (var error in chunkResult.Errors)
{
Console.WriteLine($" Error: {error}");
}
}
}
report.PassedChunks = passedCount;
report.FailedChunks = failedCount;
report.IsValid = failedCount == 0;
// Output report
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(JsonSerializer.Serialize(report, JsonOptions));
}
else
{
Console.WriteLine($"Results:");
Console.WriteLine($" Passed: {passedCount}");
Console.WriteLine($" Failed: {failedCount}");
Console.WriteLine();
Console.WriteLine($"Chain Status: {(report.IsValid ? " VALID" : " INVALID")}");
}
// Save report if requested
if (!string.IsNullOrEmpty(reportPath))
{
var reportJson = JsonSerializer.Serialize(report, JsonOptions);
await File.WriteAllTextAsync(reportPath, reportJson, ct);
if (!format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"Report saved to: {reportPath}");
}
}
return report.IsValid ? 0 : 1;
});
return verifyChainCommand;
}
#endregion
#region Sample Data
private static List<RuntimeSignal> GetSignals(string target)
@@ -362,5 +609,74 @@ public static class SignalsCommandGroup
public int ReachableVulnerabilities { get; set; }
}
// SIGNING-002 DTOs for chain verification
private sealed class ChainVerificationReport
{
public string Path { get; set; } = string.Empty;
public DateTimeOffset VerifiedAt { get; set; }
public bool OfflineMode { get; set; }
public int TotalChunks { get; set; }
public int PassedChunks { get; set; }
public int FailedChunks { get; set; }
public bool IsValid { get; set; }
public List<ChunkVerificationResult> ChunkResults { get; set; } = [];
}
private sealed class ChunkVerificationResult
{
public string FilePath { get; set; } = string.Empty;
public string? ChunkId { get; set; }
public int? ChunkSequence { get; set; }
public long? EventCount { get; set; }
public TimeRangeInfo? TimeRange { get; set; }
public bool Passed { get; set; }
public List<string> Errors { get; set; } = [];
}
private sealed class TimeRangeInfo
{
public DateTimeOffset? Start { get; set; }
public DateTimeOffset? End { get; set; }
}
private sealed class DsseEnvelopeInfo
{
public string PayloadType { get; set; } = string.Empty;
public string Payload { get; set; } = string.Empty;
public List<DsseSignatureInfo>? Signatures { get; set; }
}
private sealed class DsseSignatureInfo
{
public string? KeyId { get; set; }
public string Sig { get; set; } = string.Empty;
}
private sealed class InTotoStatementInfo
{
[JsonPropertyName("_type")]
public string? Type { get; set; }
public string? PredicateType { get; set; }
public RuntimeEvidencePredicateInfo? Predicate { get; set; }
}
private sealed class RuntimeEvidencePredicateInfo
{
[JsonPropertyName("chunk_id")]
public string? ChunkId { get; set; }
[JsonPropertyName("chunk_sequence")]
public int ChunkSequence { get; set; }
[JsonPropertyName("previous_chunk_id")]
public string? PreviousChunkId { get; set; }
[JsonPropertyName("event_count")]
public long EventCount { get; set; }
[JsonPropertyName("time_range")]
public TimeRangeInfo? TimeRange { get; set; }
}
#endregion
}