This commit is contained in:
StellaOps Bot
2026-01-02 11:47:13 +02:00
80 changed files with 15087 additions and 5608 deletions

View File

@@ -0,0 +1,33 @@
using System.CommandLine;
using StellaOps.Policy;
namespace StellaOps.Policy.Tools;
public static class PolicyDslValidatorApp
{
public static Task<int> RunAsync(string[] args)
{
var runner = new PolicyValidationRunner(new PolicyValidationCli());
return RunAsync(args, runner);
}
public static async Task<int> RunAsync(string[] args, IPolicyValidationRunner runner)
{
if (runner is null)
{
throw new ArgumentNullException(nameof(runner));
}
var root = PolicyDslValidatorCommand.Build(runner);
var parseResult = root.Parse(args, new ParserConfiguration());
var invocationConfiguration = new InvocationConfiguration();
if (parseResult.Errors.Count > 0)
{
await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
return 64; // EX_USAGE
}
return await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
}
}

View File

@@ -0,0 +1,57 @@
using System.CommandLine;
using StellaOps.Policy;
namespace StellaOps.Policy.Tools;
public static class PolicyDslValidatorCommand
{
public static RootCommand Build(IPolicyValidationRunner runner, CancellationToken? cancellationTokenOverride = null)
{
var root = new RootCommand("Validate StellaOps policy DSL files.");
Configure(root, runner, cancellationTokenOverride);
return root;
}
public static Command BuildCommand(IPolicyValidationRunner runner, CancellationToken? cancellationTokenOverride = null)
{
var command = new Command("policy-dsl-validate", "Validate StellaOps policy DSL files.");
Configure(command, runner, cancellationTokenOverride);
return command;
}
private static void Configure(Command command, IPolicyValidationRunner runner, CancellationToken? cancellationTokenOverride)
{
var inputs = new Argument<List<string>>("inputs")
{
Description = "Policy files, directories, or globs to validate.",
Arity = ArgumentArity.OneOrMore
};
var strict = new Option<bool>("--strict", new[] { "-s" })
{
Description = "Treat warnings as errors."
};
var outputJson = new Option<bool>("--json", new[] { "-j" })
{
Description = "Emit machine-readable JSON output."
};
command.Add(inputs);
command.Add(strict);
command.Add(outputJson);
command.SetAction(async (parseResult, cancellationToken) =>
{
var options = new PolicyValidationCliOptions
{
Inputs = parseResult.GetValue(inputs) ?? new List<string>(),
Strict = parseResult.GetValue(strict),
OutputJson = parseResult.GetValue(outputJson),
};
var effectiveCancellationToken = cancellationTokenOverride ?? cancellationToken;
return await runner.RunAsync(options, effectiveCancellationToken);
});
}
}

View File

@@ -0,0 +1,22 @@
using System.CommandLine;
namespace StellaOps.Policy.Tools;
public static class PolicySchemaExporterApp
{
public static async Task<int> RunAsync(string[] args)
{
var runner = new PolicySchemaExporterRunner();
var root = PolicySchemaExporterCommand.Build(runner);
var parseResult = root.Parse(args, new ParserConfiguration());
var invocationConfiguration = new InvocationConfiguration();
if (parseResult.Errors.Count > 0)
{
await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
return 64; // EX_USAGE
}
return await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
}
}

View File

@@ -0,0 +1,48 @@
using System.CommandLine;
namespace StellaOps.Policy.Tools;
public static class PolicySchemaExporterCommand
{
public static RootCommand Build(PolicySchemaExporterRunner runner, CancellationToken? cancellationTokenOverride = null)
{
var root = new RootCommand("Export policy schema JSON files.");
Configure(root, runner, cancellationTokenOverride);
return root;
}
public static Command BuildCommand(PolicySchemaExporterRunner runner, CancellationToken? cancellationTokenOverride = null)
{
var command = new Command("policy-schema-export", "Export policy schema JSON files.");
Configure(command, runner, cancellationTokenOverride);
return command;
}
private static void Configure(Command command, PolicySchemaExporterRunner runner, CancellationToken? cancellationTokenOverride)
{
var output = new Option<string?>("--output", new[] { "-o" })
{
Description = "Output directory for schema files."
};
var repoRoot = new Option<string?>("--repo-root", new[] { "-r" })
{
Description = "Repository root used to resolve default output path."
};
command.Add(output);
command.Add(repoRoot);
command.SetAction((parseResult, cancellationToken) =>
{
var options = new PolicySchemaExportOptions
{
OutputDirectory = parseResult.GetValue(output),
RepoRoot = parseResult.GetValue(repoRoot),
};
var effectiveCancellationToken = cancellationTokenOverride ?? cancellationToken;
return runner.RunAsync(options, effectiveCancellationToken);
});
}
}

View File

@@ -0,0 +1,190 @@
using System.Collections.Immutable;
using NJsonSchema;
using NJsonSchema.Generation;
using Newtonsoft.Json;
using StellaOps.Scheduler.Models;
namespace StellaOps.Policy.Tools;
public sealed record PolicySchemaExportOptions
{
public string? OutputDirectory { get; init; }
public string? RepoRoot { get; init; }
}
public sealed record SchemaExportDefinition(string FileName, Type Type);
public sealed class PolicySchemaExporterRunner
{
public async Task<int> RunAsync(PolicySchemaExportOptions options, CancellationToken cancellationToken)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
var repoRoot = NormalizePath(options.RepoRoot)
?? PolicySchemaExporterPaths.TryFindRepoRoot(Directory.GetCurrentDirectory())
?? PolicySchemaExporterPaths.TryFindRepoRoot(AppContext.BaseDirectory);
string? outputDirectory;
if (!string.IsNullOrWhiteSpace(options.OutputDirectory))
{
outputDirectory = PolicySchemaExporterPaths.ResolveOutputDirectory(options.OutputDirectory!, repoRoot);
}
else if (!string.IsNullOrWhiteSpace(repoRoot))
{
outputDirectory = PolicySchemaExporterPaths.ResolveDefaultOutputDirectory(repoRoot);
}
else
{
Console.Error.WriteLine("Unable to resolve repo root. Provide --output or --repo-root.");
return 64; // EX_USAGE
}
if (!TryEnsureOutputDirectory(outputDirectory, out var error))
{
Console.Error.WriteLine(error);
return 73; // EX_CANTCREAT
}
var generator = PolicySchemaExporterSchema.CreateGenerator();
var exports = PolicySchemaExporterSchema.BuildExports();
var schemas = PolicySchemaExporterSchema.GenerateSchemas(generator, exports);
foreach (var export in exports)
{
if (!schemas.TryGetValue(export.FileName, out var json))
{
continue;
}
var outputPath = Path.Combine(outputDirectory, export.FileName);
await File.WriteAllTextAsync(outputPath, json + Environment.NewLine, cancellationToken);
Console.WriteLine($"Wrote {outputPath}");
}
return 0;
}
private static string? NormalizePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
return Path.GetFullPath(path);
}
private static bool TryEnsureOutputDirectory(string outputDirectory, out string? error)
{
error = null;
try
{
if (File.Exists(outputDirectory))
{
error = $"Output path '{outputDirectory}' is a file, expected a directory.";
return false;
}
Directory.CreateDirectory(outputDirectory);
return true;
}
catch (Exception ex)
{
error = $"Failed to create output directory '{outputDirectory}': {ex.Message}";
return false;
}
}
}
public static class PolicySchemaExporterSchema
{
public static ImmutableArray<SchemaExportDefinition> BuildExports()
=> ImmutableArray.Create(
new SchemaExportDefinition("policy-run-request.schema.json", typeof(PolicyRunRequest)),
new SchemaExportDefinition("policy-run-status.schema.json", typeof(PolicyRunStatus)),
new SchemaExportDefinition("policy-diff-summary.schema.json", typeof(PolicyDiffSummary)),
new SchemaExportDefinition("policy-explain-trace.schema.json", typeof(PolicyExplainTrace))
);
public static JsonSchemaGenerator CreateGenerator()
{
var generatorSettings = new NJsonSchema.NewtonsoftJson.Generation.NewtonsoftJsonSchemaGeneratorSettings
{
SchemaType = SchemaType.JsonSchema,
DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull,
SerializerSettings = new JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
},
};
return new JsonSchemaGenerator(generatorSettings);
}
public static IReadOnlyDictionary<string, string> GenerateSchemas(JsonSchemaGenerator generator, IEnumerable<SchemaExportDefinition> exports)
{
if (generator is null)
{
throw new ArgumentNullException(nameof(generator));
}
if (exports is null)
{
throw new ArgumentNullException(nameof(exports));
}
var results = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var export in exports)
{
var schema = generator.Generate(export.Type);
schema.Title = export.Type.Name;
schema.AllowAdditionalProperties = false;
results[export.FileName] = schema.ToJson(Formatting.Indented);
}
return results;
}
}
public static class PolicySchemaExporterPaths
{
public static string? TryFindRepoRoot(string startDirectory)
{
if (string.IsNullOrWhiteSpace(startDirectory))
{
return null;
}
var current = new DirectoryInfo(Path.GetFullPath(startDirectory));
while (current is not null)
{
var candidate = Path.Combine(current.FullName, "src", "Directory.Build.props");
if (File.Exists(candidate))
{
return current.FullName;
}
current = current.Parent;
}
return null;
}
public static string ResolveDefaultOutputDirectory(string repoRoot)
=> Path.GetFullPath(Path.Combine(repoRoot, "docs", "schemas"));
public static string ResolveOutputDirectory(string outputPath, string? repoRoot)
{
if (Path.IsPathRooted(outputPath))
{
return Path.GetFullPath(outputPath);
}
var baseDirectory = !string.IsNullOrWhiteSpace(repoRoot) ? repoRoot : Directory.GetCurrentDirectory();
return Path.GetFullPath(Path.Combine(baseDirectory, outputPath));
}
}

View File

@@ -0,0 +1,22 @@
using System.CommandLine;
namespace StellaOps.Policy.Tools;
public static class PolicySimulationSmokeApp
{
public static async Task<int> RunAsync(string[] args)
{
var runner = new PolicySimulationSmokeRunner();
var root = PolicySimulationSmokeCommand.Build(runner);
var parseResult = root.Parse(args, new ParserConfiguration());
var invocationConfiguration = new InvocationConfiguration();
if (parseResult.Errors.Count > 0)
{
await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
return 64; // EX_USAGE
}
return await parseResult.InvokeAsync(invocationConfiguration, CancellationToken.None);
}
}

View File

@@ -0,0 +1,75 @@
using System.CommandLine;
namespace StellaOps.Policy.Tools;
public static class PolicySimulationSmokeCommand
{
public static RootCommand Build(PolicySimulationSmokeRunner runner, CancellationToken? cancellationTokenOverride = null)
{
var root = new RootCommand("Run policy simulation smoke scenarios.");
Configure(root, runner, cancellationTokenOverride);
return root;
}
public static Command BuildCommand(PolicySimulationSmokeRunner runner, CancellationToken? cancellationTokenOverride = null)
{
var command = new Command("policy-simulation-smoke", "Run policy simulation smoke scenarios.");
Configure(command, runner, cancellationTokenOverride);
return command;
}
private static void Configure(Command command, PolicySimulationSmokeRunner runner, CancellationToken? cancellationTokenOverride)
{
var scenarioRoot = new Option<string>("--scenario-root", new[] { "-r" })
{
Description = "Path to the policy simulation scenarios."
};
var output = new Option<string?>("--output", new[] { "-o" })
{
Description = "Directory for summary output."
};
var repoRoot = new Option<string?>("--repo-root", Array.Empty<string>())
{
Description = "Repository root for resolving relative paths."
};
var fixedTime = new Option<string?>("--fixed-time", Array.Empty<string>())
{
Description = "Fixed ISO-8601 timestamp for deterministic runs."
};
command.Add(scenarioRoot);
command.Add(output);
command.Add(repoRoot);
command.Add(fixedTime);
command.SetAction(async (parseResult, cancellationToken) =>
{
var fixedTimeValue = parseResult.GetValue(fixedTime);
DateTimeOffset? fixedTimeParsed = null;
if (!string.IsNullOrWhiteSpace(fixedTimeValue))
{
if (!PolicySimulationSmokeParsing.TryParseFixedTime(fixedTimeValue!, out var parsed))
{
Console.Error.WriteLine("Invalid --fixed-time value. Use ISO-8601 (e.g., 2025-01-02T03:04:05Z).");
return 64; // EX_USAGE
}
fixedTimeParsed = parsed;
}
var options = new PolicySimulationSmokeOptions
{
ScenarioRoot = parseResult.GetValue(scenarioRoot) ?? "samples/policy/simulations",
OutputDirectory = parseResult.GetValue(output),
RepoRoot = parseResult.GetValue(repoRoot),
FixedTime = fixedTimeParsed,
};
var effectiveCancellationToken = cancellationTokenOverride ?? cancellationToken;
return await runner.RunAsync(options, effectiveCancellationToken);
});
}
}

View File

@@ -0,0 +1,74 @@
using StellaOps.Policy;
namespace StellaOps.Policy.Tools;
public sealed record PolicySimulationScenario
{
public string Name { get; init; } = "scenario";
public string PolicyPath { get; init; } = string.Empty;
public List<ScenarioFinding> Findings { get; init; } = new();
public List<ScenarioExpectedDiff> ExpectedDiffs { get; init; } = new();
public List<ScenarioBaseline>? Baseline { get; init; }
}
public sealed record ScenarioFinding
{
public string FindingId { get; init; } = string.Empty;
public string Severity { get; init; } = "Low";
public string? Environment { get; init; }
public string? Source { get; init; }
public string? Vendor { get; init; }
public string? License { get; init; }
public string? Image { get; init; }
public string? Repository { get; init; }
public string? Package { get; init; }
public string? Purl { get; init; }
public string? Cve { get; init; }
public string? Path { get; init; }
public string? LayerDigest { get; init; }
public string[]? Tags { get; init; }
}
public sealed record ScenarioExpectedDiff
{
public string FindingId { get; init; } = string.Empty;
public string Status { get; init; } = "Pass";
}
public sealed record ScenarioBaseline
{
public string FindingId { get; init; } = string.Empty;
public string Status { get; init; } = "Pass";
public string? RuleName { get; init; }
public string? RuleAction { get; init; }
public string? Notes { get; init; }
public double Score { get; init; }
public string? ConfigVersion { get; init; }
public Dictionary<string, double>? Inputs { get; init; }
}
public sealed record ScenarioResult(string ScenarioName)
{
public bool Success { get; init; } = true;
public int ChangedCount { get; init; }
public List<string> Failures { get; } = new();
public Dictionary<string, string> ActualStatuses { get; } = new(StringComparer.OrdinalIgnoreCase);
}
public sealed class NullPolicySnapshotRepository : IPolicySnapshotRepository
{
public Task AddAsync(PolicySnapshot snapshot, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task<PolicySnapshot?> GetLatestAsync(CancellationToken cancellationToken = default) => Task.FromResult<PolicySnapshot?>(null);
public Task<IReadOnlyList<PolicySnapshot>> ListAsync(int limit, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<PolicySnapshot>>(Array.Empty<PolicySnapshot>());
}
public sealed class NullPolicyAuditRepository : IPolicyAuditRepository
{
public Task AddAsync(PolicyAuditEntry entry, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task<IReadOnlyList<PolicyAuditEntry>> ListAsync(int limit, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<PolicyAuditEntry>>(Array.Empty<PolicyAuditEntry>());
}

View File

@@ -0,0 +1,338 @@
using System.Collections.Immutable;
using System.Globalization;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Policy;
namespace StellaOps.Policy.Tools;
public sealed record PolicySimulationSmokeOptions
{
public string ScenarioRoot { get; init; } = "samples/policy/simulations";
public string? OutputDirectory { get; init; }
public string? RepoRoot { get; init; }
public DateTimeOffset? FixedTime { get; init; }
}
public sealed class PolicySimulationSmokeRunner
{
private readonly ILoggerFactory _loggerFactory;
public PolicySimulationSmokeRunner(ILoggerFactory? loggerFactory = null)
{
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
}
public async Task<int> RunAsync(PolicySimulationSmokeOptions options, CancellationToken cancellationToken)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
var repoRoot = PolicySimulationSmokePaths.ResolveRepoRoot(options.RepoRoot);
var scenarioRoot = PolicySimulationSmokePaths.ResolveScenarioRoot(options.ScenarioRoot, repoRoot);
if (scenarioRoot is null)
{
Console.Error.WriteLine("Scenario root is relative; provide --repo-root or use an absolute path.");
return 64; // EX_USAGE
}
if (!Directory.Exists(scenarioRoot))
{
Console.Error.WriteLine($"Scenario root '{scenarioRoot}' does not exist.");
return 66; // EX_NOINPUT
}
var scenarioFiles = Directory.GetFiles(scenarioRoot, "scenario.json", SearchOption.AllDirectories)
.OrderBy(static path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (scenarioFiles.Length == 0)
{
Console.Error.WriteLine($"No scenario.json files found under '{scenarioRoot}'.");
return 0;
}
var timeProvider = options.FixedTime.HasValue
? new FixedTimeProvider(options.FixedTime.Value)
: TimeProvider.System;
var snapshotStore = new PolicySnapshotStore(
new NullPolicySnapshotRepository(),
new NullPolicyAuditRepository(),
timeProvider,
_loggerFactory.CreateLogger<PolicySnapshotStore>());
var previewService = new PolicyPreviewService(snapshotStore, _loggerFactory.CreateLogger<PolicyPreviewService>());
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
};
var summary = new List<ScenarioResult>();
var success = true;
foreach (var scenarioFile in scenarioFiles)
{
cancellationToken.ThrowIfCancellationRequested();
var scenarioText = await File.ReadAllTextAsync(scenarioFile, cancellationToken);
var scenario = JsonSerializer.Deserialize<PolicySimulationScenario>(scenarioText, serializerOptions);
if (scenario is null)
{
Console.Error.WriteLine($"Failed to deserialize scenario '{scenarioFile}'.");
success = false;
continue;
}
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.");
success = false;
continue;
}
if (!File.Exists(policyPath))
{
Console.Error.WriteLine($"Policy file '{scenario.PolicyPath}' referenced by scenario '{scenario.Name}' does not exist.");
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 request = new PolicyPreviewRequest(
ImageDigest: $"sha256:simulation-{scenario.Name}",
Findings: findings,
BaselineVerdicts: baseline,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(
Content: policyContent,
Format: policyFormat,
Actor: "ci",
Source: "ci/simulation-smoke",
Description: $"CI simulation for scenario '{scenario.Name}'"));
var response = await previewService.PreviewAsync(request, cancellationToken);
var scenarioResult = PolicySimulationSmokeEvaluator.EvaluateScenario(scenario, response);
summary.Add(scenarioResult);
if (!scenarioResult.Success)
{
success = false;
}
}
if (options.OutputDirectory is not null)
{
var outputDirectory = PolicySimulationSmokePaths.ResolveOutputDirectory(options.OutputDirectory, repoRoot);
if (outputDirectory is null)
{
Console.Error.WriteLine("Output path is relative; provide --repo-root or use an absolute path.");
return 64; // EX_USAGE
}
Directory.CreateDirectory(outputDirectory);
var summaryPath = Path.Combine(outputDirectory, "policy-simulation-summary.json");
var summaryJson = JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(summaryPath, summaryJson, cancellationToken);
}
return success ? 0 : 1;
}
private static PolicyFinding ToPolicyFinding(ScenarioFinding finding)
{
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(
finding.FindingId,
severity,
finding.Environment,
finding.Source,
finding.Vendor,
finding.License,
finding.Image,
finding.Repository,
finding.Package,
finding.Purl,
finding.Cve,
finding.Path,
finding.LayerDigest,
tags);
}
private static PolicyVerdict ToPolicyVerdict(ScenarioBaseline baseline)
{
var status = Enum.Parse<PolicyVerdictStatus>(baseline.Status, ignoreCase: true);
var inputs = baseline.Inputs?.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase) ?? ImmutableDictionary<string, double>.Empty;
return new PolicyVerdict(
baseline.FindingId,
status,
RuleName: baseline.RuleName,
RuleAction: baseline.RuleAction,
Notes: baseline.Notes,
Score: baseline.Score,
ConfigVersion: baseline.ConfigVersion ?? PolicyScoringConfig.Default.Version,
Inputs: inputs,
QuietedBy: null,
Quiet: false,
UnknownConfidence: null,
ConfidenceBand: null,
UnknownAgeDays: null,
SourceTrust: null,
Reachability: null);
}
}
public static class PolicySimulationSmokeEvaluator
{
public static ScenarioResult EvaluateScenario(PolicySimulationScenario scenario, PolicyPreviewResponse response)
{
var result = new ScenarioResult(scenario.Name);
if (!response.Success)
{
result.Failures.Add("Preview failed.");
return result with { Success = false, ChangedCount = response.ChangedCount };
}
var diffs = response.Diffs.ToDictionary(diff => diff.Projected.FindingId, StringComparer.OrdinalIgnoreCase);
foreach (var expected in scenario.ExpectedDiffs)
{
if (!diffs.TryGetValue(expected.FindingId, out var diff))
{
result.Failures.Add($"Expected finding '{expected.FindingId}' missing from diff.");
continue;
}
var projectedStatus = diff.Projected.Status.ToString();
result.ActualStatuses[expected.FindingId] = projectedStatus;
if (!string.Equals(projectedStatus, expected.Status, StringComparison.OrdinalIgnoreCase))
{
result.Failures.Add($"Finding '{expected.FindingId}' expected status '{expected.Status}' but was '{projectedStatus}'.");
}
}
foreach (var diff in diffs.Values)
{
if (!result.ActualStatuses.ContainsKey(diff.Projected.FindingId))
{
result.ActualStatuses[diff.Projected.FindingId] = diff.Projected.Status.ToString();
}
}
var success = result.Failures.Count == 0;
return result with
{
Success = success,
ChangedCount = response.ChangedCount
};
}
}
public static class PolicySimulationSmokePaths
{
public static string? ResolveRepoRoot(string? explicitRepoRoot)
{
if (!string.IsNullOrWhiteSpace(explicitRepoRoot))
{
return Path.GetFullPath(explicitRepoRoot);
}
return TryFindRepoRoot(Directory.GetCurrentDirectory())
?? TryFindRepoRoot(AppContext.BaseDirectory);
}
public static string? ResolveScenarioRoot(string scenarioRoot, string? repoRoot)
{
if (Path.IsPathRooted(scenarioRoot))
{
return Path.GetFullPath(scenarioRoot);
}
if (string.IsNullOrWhiteSpace(repoRoot))
{
return null;
}
return Path.GetFullPath(Path.Combine(repoRoot, scenarioRoot));
}
public static string? ResolvePolicyPath(string policyPath, string? repoRoot)
{
if (Path.IsPathRooted(policyPath))
{
return Path.GetFullPath(policyPath);
}
if (string.IsNullOrWhiteSpace(repoRoot))
{
return null;
}
return Path.GetFullPath(Path.Combine(repoRoot, policyPath));
}
public static string? ResolveOutputDirectory(string outputDirectory, string? repoRoot)
{
if (Path.IsPathRooted(outputDirectory))
{
return Path.GetFullPath(outputDirectory);
}
var baseDirectory = !string.IsNullOrWhiteSpace(repoRoot) ? repoRoot : Directory.GetCurrentDirectory();
return Path.GetFullPath(Path.Combine(baseDirectory, outputDirectory));
}
public static string? TryFindRepoRoot(string startDirectory)
{
if (string.IsNullOrWhiteSpace(startDirectory))
{
return null;
}
var current = new DirectoryInfo(Path.GetFullPath(startDirectory));
while (current is not null)
{
var candidate = Path.Combine(current.FullName, "src", "Directory.Build.props");
if (File.Exists(candidate))
{
return current.FullName;
}
current = current.Parent;
}
return null;
}
}
public static class PolicySimulationSmokeParsing
{
public static bool TryParseFixedTime(string value, out DateTimeOffset fixedTime)
=> DateTimeOffset.TryParse(
value,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out fixedTime);
}
public sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _fixedTime;
public FixedTimeProvider(DateTimeOffset fixedTime)
{
_fixedTime = fixedTime.ToUniversalTime();
}
public override DateTimeOffset GetUtcNow() => _fixedTime;
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
}

View File

@@ -0,0 +1,21 @@
using StellaOps.Policy;
namespace StellaOps.Policy.Tools;
public interface IPolicyValidationRunner
{
Task<int> RunAsync(PolicyValidationCliOptions options, CancellationToken cancellationToken);
}
public sealed class PolicyValidationRunner : IPolicyValidationRunner
{
private readonly PolicyValidationCli _cli;
public PolicyValidationRunner(PolicyValidationCli cli)
{
_cli = cli ?? throw new ArgumentNullException(nameof(cli));
}
public Task<int> RunAsync(PolicyValidationCliOptions options, CancellationToken cancellationToken)
=> _cli.RunAsync(options, cancellationToken);
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NJsonSchema" />
<PackageReference Include="NJsonSchema.CodeGeneration.CSharp" />
<PackageReference Include="NJsonSchema.NewtonsoftJson" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Policy\__Libraries\StellaOps.Policy\StellaOps.Policy.csproj" />
<ProjectReference Include="..\..\Scheduler\__Libraries\StellaOps.Scheduler.Models\StellaOps.Scheduler.Models.csproj" />
</ItemGroup>
</Project>