audit, advisories and doctors/setup work
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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). |
|
||||
|
||||
Reference in New Issue
Block a user