audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -21,13 +21,27 @@ public static class PolicyDslValidatorApp
var root = PolicyDslValidatorCommand.Build(runner);
var parseResult = root.Parse(args, new ParserConfiguration());
var invocationConfiguration = new InvocationConfiguration();
if (parseResult.Errors.Count > 0)
using var cancellationSource = new CancellationTokenSource();
ConsoleCancelEventHandler? handler = (_, e) =>
{
await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
return 64; // EX_USAGE
}
e.Cancel = true;
cancellationSource.Cancel();
};
Console.CancelKeyPress += handler;
return await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
try
{
if (parseResult.Errors.Count > 0)
{
await parseResult.InvokeAsync(invocationConfiguration, cancellationSource.Token);
return 64; // EX_USAGE
}
return await parseResult.InvokeAsync(invocationConfiguration, cancellationSource.Token);
}
finally
{
Console.CancelKeyPress -= handler;
}
}
}

View File

@@ -10,13 +10,27 @@ public static class PolicySchemaExporterApp
var root = PolicySchemaExporterCommand.Build(runner);
var parseResult = root.Parse(args, new ParserConfiguration());
var invocationConfiguration = new InvocationConfiguration();
if (parseResult.Errors.Count > 0)
using var cancellationSource = new CancellationTokenSource();
ConsoleCancelEventHandler? handler = (_, e) =>
{
await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
return 64; // EX_USAGE
}
e.Cancel = true;
cancellationSource.Cancel();
};
Console.CancelKeyPress += handler;
return await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
try
{
if (parseResult.Errors.Count > 0)
{
await parseResult.InvokeAsync(invocationConfiguration, cancellationSource.Token);
return 64; // EX_USAGE
}
return await parseResult.InvokeAsync(invocationConfiguration, cancellationSource.Token);
}
finally
{
Console.CancelKeyPress -= handler;
}
}
}

View File

@@ -60,7 +60,7 @@ public sealed class PolicySchemaExporterRunner
}
var outputPath = Path.Combine(outputDirectory, export.FileName);
await File.WriteAllTextAsync(outputPath, json + Environment.NewLine, cancellationToken);
await File.WriteAllTextAsync(outputPath, json + "\n", cancellationToken);
Console.WriteLine($"Wrote {outputPath}");
}

View File

@@ -10,13 +10,27 @@ public static class PolicySimulationSmokeApp
var root = PolicySimulationSmokeCommand.Build(runner);
var parseResult = root.Parse(args, new ParserConfiguration());
var invocationConfiguration = new InvocationConfiguration();
if (parseResult.Errors.Count > 0)
using var cancellationSource = new CancellationTokenSource();
ConsoleCancelEventHandler? handler = (_, e) =>
{
await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
return 64; // EX_USAGE
}
e.Cancel = true;
cancellationSource.Cancel();
};
Console.CancelKeyPress += handler;
return await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
try
{
if (parseResult.Errors.Count > 0)
{
await parseResult.InvokeAsync(invocationConfiguration, cancellationSource.Token);
return 64; // EX_USAGE
}
return await parseResult.InvokeAsync(invocationConfiguration, cancellationSource.Token);
}
finally
{
Console.CancelKeyPress -= handler;
}
}
}

View File

@@ -15,6 +15,14 @@ public sealed record PolicySimulationSmokeOptions
public DateTimeOffset? FixedTime { get; init; }
}
internal static class PolicySimulationSmokeDefaults
{
public static readonly DateTimeOffset DefaultFixedTime = DateTimeOffset.UnixEpoch;
public static DateTimeOffset ResolveFixedTime(DateTimeOffset? fixedTime)
=> fixedTime ?? DefaultFixedTime;
}
public sealed class PolicySimulationSmokeRunner
{
private readonly ILoggerFactory _loggerFactory;
@@ -54,9 +62,8 @@ public sealed class PolicySimulationSmokeRunner
return 0;
}
var timeProvider = options.FixedTime.HasValue
? new FixedTimeProvider(options.FixedTime.Value)
: TimeProvider.System;
var fixedTime = PolicySimulationSmokeDefaults.ResolveFixedTime(options.FixedTime);
var timeProvider = new FixedTimeProvider(fixedTime);
var snapshotStore = new PolicySnapshotStore(
new NullPolicySnapshotRepository(),
@@ -64,7 +71,10 @@ public sealed class PolicySimulationSmokeRunner
timeProvider,
null,
_loggerFactory.CreateLogger<PolicySnapshotStore>());
var previewService = new PolicyPreviewService(snapshotStore, _loggerFactory.CreateLogger<PolicyPreviewService>());
var previewService = new PolicyPreviewService(
snapshotStore,
_loggerFactory.CreateLogger<PolicyPreviewService>(),
timeProvider);
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
@@ -80,10 +90,28 @@ public sealed class PolicySimulationSmokeRunner
cancellationToken.ThrowIfCancellationRequested();
var scenarioText = await File.ReadAllTextAsync(scenarioFile, cancellationToken);
var scenario = JsonSerializer.Deserialize<PolicySimulationScenario>(scenarioText, serializerOptions);
PolicySimulationScenario? scenario;
string? scenarioError = null;
try
{
scenario = JsonSerializer.Deserialize<PolicySimulationScenario>(scenarioText, serializerOptions);
}
catch (JsonException ex)
{
scenarioError = ex.Message;
scenario = null;
}
var scenarioName = ResolveScenarioName(scenario, scenarioFile, scenarioRoot);
var scenarioResult = new ScenarioResult(scenarioName);
if (scenario is null)
{
Console.Error.WriteLine($"Failed to deserialize scenario '{scenarioFile}'.");
var message = scenarioError is null
? $"Failed to deserialize scenario '{scenarioFile}'."
: $"Failed to deserialize scenario '{scenarioFile}': {scenarioError}";
AddFailure(scenarioResult, message);
summary.Add(scenarioResult with { Success = false });
success = false;
continue;
}
@@ -91,26 +119,73 @@ public sealed class PolicySimulationSmokeRunner
var policyPath = PolicySimulationSmokePaths.ResolvePolicyPath(scenario.PolicyPath, repoRoot);
if (policyPath is null)
{
Console.Error.WriteLine($"Policy path '{scenario.PolicyPath}' is relative; provide --repo-root or use an absolute path.");
var message = $"Scenario '{scenarioName}' policy path '{scenario.PolicyPath}' is relative; provide --repo-root or use an absolute path.";
AddFailure(scenarioResult, message);
summary.Add(scenarioResult with { Success = false });
success = false;
continue;
}
if (!File.Exists(policyPath))
{
Console.Error.WriteLine($"Policy file '{scenario.PolicyPath}' referenced by scenario '{scenario.Name}' does not exist.");
var message = $"Scenario '{scenarioName}' policy file '{scenario.PolicyPath}' does not exist.";
AddFailure(scenarioResult, message);
summary.Add(scenarioResult with { Success = false });
success = false;
continue;
}
var policyContent = await File.ReadAllTextAsync(policyPath, cancellationToken);
var policyFormat = PolicySchema.DetectFormat(policyPath);
var findings = scenario.Findings.Select(ToPolicyFinding).ToImmutableArray();
var baseline = scenario.Baseline?.Select(ToPolicyVerdict).ToImmutableArray() ?? ImmutableArray<PolicyVerdict>.Empty;
var findings = ImmutableArray.CreateBuilder<PolicyFinding>(scenario.Findings.Count);
var hasErrors = false;
foreach (var finding in scenario.Findings)
{
if (!TryBuildFinding(finding, scenarioName, out var policyFinding, out var error))
{
AddFailure(scenarioResult, error ?? "Unknown error building finding");
hasErrors = true;
continue;
}
findings.Add(policyFinding);
}
ImmutableArray<PolicyVerdict> baseline;
if (scenario.Baseline is { Count: > 0 })
{
var baselineBuilder = ImmutableArray.CreateBuilder<PolicyVerdict>(scenario.Baseline.Count);
foreach (var entry in scenario.Baseline)
{
if (!TryBuildVerdict(entry, scenarioName, out var verdict, out var error))
{
AddFailure(scenarioResult, error ?? "Unknown error building verdict");
hasErrors = true;
continue;
}
baselineBuilder.Add(verdict);
}
baseline = baselineBuilder.ToImmutable();
}
else
{
baseline = ImmutableArray<PolicyVerdict>.Empty;
}
if (hasErrors)
{
summary.Add(scenarioResult with { Success = false });
success = false;
continue;
}
var scenarioIdentifier = NormalizeScenarioIdentifier(scenarioName);
var request = new PolicyPreviewRequest(
ImageDigest: $"sha256:simulation-{scenario.Name}",
Findings: findings,
ImageDigest: $"sha256:simulation-{scenarioIdentifier}",
Findings: findings.ToImmutable(),
BaselineVerdicts: baseline,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(
@@ -118,13 +193,13 @@ public sealed class PolicySimulationSmokeRunner
Format: policyFormat,
Actor: "ci",
Source: "ci/simulation-smoke",
Description: $"CI simulation for scenario '{scenario.Name}'"));
Description: $"CI simulation for scenario '{scenarioName}'"));
var response = await previewService.PreviewAsync(request, cancellationToken);
var scenarioResult = PolicySimulationSmokeEvaluator.EvaluateScenario(scenario, response);
summary.Add(scenarioResult);
var evaluatedResult = PolicySimulationSmokeEvaluator.EvaluateScenario(scenario, response);
summary.Add(evaluatedResult);
if (!scenarioResult.Success)
if (!evaluatedResult.Success)
{
success = false;
}
@@ -141,18 +216,53 @@ public sealed class PolicySimulationSmokeRunner
Directory.CreateDirectory(outputDirectory);
var summaryPath = Path.Combine(outputDirectory, "policy-simulation-summary.json");
var summaryJson = JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true });
var summaryOutput = BuildSummaryOutput(summary);
var summaryJson = JsonSerializer.Serialize(summaryOutput, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(summaryPath, summaryJson, cancellationToken);
}
return success ? 0 : 1;
}
private static PolicyFinding ToPolicyFinding(ScenarioFinding finding)
private static string ResolveScenarioName(PolicySimulationScenario? scenario, string scenarioFile, string scenarioRoot)
{
if (!string.IsNullOrWhiteSpace(scenario?.Name))
{
return scenario!.Name;
}
var relative = Path.GetRelativePath(scenarioRoot, scenarioFile);
return string.IsNullOrWhiteSpace(relative) ? scenarioFile : relative;
}
private static string NormalizeScenarioIdentifier(string scenarioName)
=> scenarioName
.Replace(Path.DirectorySeparatorChar, '-')
.Replace(Path.AltDirectorySeparatorChar, '-');
private static void AddFailure(ScenarioResult result, string message)
{
result.Failures.Add(message);
Console.Error.WriteLine(message);
}
private static bool TryBuildFinding(
ScenarioFinding finding,
string scenarioName,
out PolicyFinding policyFinding,
out string? error)
{
error = null;
policyFinding = default!;
if (!TryParseSeverity(finding.Severity, out var severity))
{
error = $"Scenario '{scenarioName}' finding '{finding.FindingId}' has invalid severity '{finding.Severity}'.";
return false;
}
var tags = finding.Tags is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(finding.Tags);
var severity = Enum.Parse<PolicySeverity>(finding.Severity, ignoreCase: true);
return new PolicyFinding(
policyFinding = new PolicyFinding(
finding.FindingId,
severity,
finding.Environment,
@@ -167,13 +277,26 @@ public sealed class PolicySimulationSmokeRunner
finding.Path,
finding.LayerDigest,
tags);
return true;
}
private static PolicyVerdict ToPolicyVerdict(ScenarioBaseline baseline)
private static bool TryBuildVerdict(
ScenarioBaseline baseline,
string scenarioName,
out PolicyVerdict verdict,
out string? error)
{
var status = Enum.Parse<PolicyVerdictStatus>(baseline.Status, ignoreCase: true);
error = null;
verdict = default!;
if (!TryParseVerdictStatus(baseline.Status, out var status))
{
error = $"Scenario '{scenarioName}' baseline '{baseline.FindingId}' has invalid status '{baseline.Status}'.";
return false;
}
var inputs = baseline.Inputs?.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase) ?? ImmutableDictionary<string, double>.Empty;
return new PolicyVerdict(
verdict = new PolicyVerdict(
baseline.FindingId,
status,
RuleName: baseline.RuleName,
@@ -189,7 +312,70 @@ public sealed class PolicySimulationSmokeRunner
UnknownAgeDays: null,
SourceTrust: null,
Reachability: null);
return true;
}
private static bool TryParseSeverity(string? value, out PolicySeverity severity)
{
severity = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return false;
}
if (!Enum.TryParse(value, ignoreCase: true, out severity))
{
return false;
}
return Enum.IsDefined(typeof(PolicySeverity), severity);
}
private static bool TryParseVerdictStatus(string? value, out PolicyVerdictStatus status)
{
status = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return false;
}
if (!Enum.TryParse(value, ignoreCase: true, out status))
{
return false;
}
return Enum.IsDefined(typeof(PolicyVerdictStatus), status);
}
private static IReadOnlyList<ScenarioResultOutput> BuildSummaryOutput(IReadOnlyList<ScenarioResult> summary)
{
var output = new List<ScenarioResultOutput>(summary.Count);
foreach (var result in summary)
{
var failures = result.Failures.Count == 0 ? new List<string>() : new List<string>(result.Failures);
var statuses = new SortedDictionary<string, string>(result.ActualStatuses, StringComparer.OrdinalIgnoreCase);
output.Add(new ScenarioResultOutput(result.ScenarioName, result.Success, result.ChangedCount, failures, statuses));
}
return output;
}
private sealed record ScenarioResultOutput(
string ScenarioName,
bool Success,
int ChangedCount,
IReadOnlyList<string> Failures,
SortedDictionary<string, string> ActualStatuses);
}
public static class PolicySimulationSmokeEvaluator

View File

@@ -16,6 +16,10 @@
<PackageReference Include="System.CommandLine" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Policy.Tools.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Policy\__Libraries\StellaOps.Policy\StellaOps.Policy.csproj" />
<ProjectReference Include="..\..\Scheduler\__Libraries\StellaOps.Scheduler.Models\StellaOps.Scheduler.Models.csproj" />

View File

@@ -7,4 +7,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| --- | --- | --- |
| AUDIT-0096-M | DONE | Revalidated 2026-01-08. |
| AUDIT-0096-T | DONE | Revalidated 2026-01-08. |
| AUDIT-0096-A | TODO | Revalidated 2026-01-08 (open findings). |
| AUDIT-0096-A | DONE | Applied 2026-01-14 (deterministic output, parsing guards, tests). |