- Added `SchedulerWorkerOptions` class to encapsulate configuration for the scheduler worker. - Introduced `PlannerBackgroundService` to manage the planner loop, fetching and processing planning runs. - Created `PlannerExecutionService` to handle the execution logic for planning runs, including impact targeting and run persistence. - Developed `PlannerExecutionResult` and `PlannerExecutionStatus` to standardize execution outcomes. - Implemented validation logic within `SchedulerWorkerOptions` to ensure proper configuration. - Added documentation for the planner loop and impact targeting features. - Established health check endpoints and authentication mechanisms for the Signals service. - Created unit tests for the Signals API to ensure proper functionality and response handling. - Configured options for authority integration and fallback authentication methods.
305 lines
9.4 KiB
C#
305 lines
9.4 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Policy;
|
|
|
|
public static class PolicyDigest
|
|
{
|
|
public static string Compute(PolicyDocument document)
|
|
{
|
|
if (document is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(document));
|
|
}
|
|
|
|
var buffer = new ArrayBufferWriter<byte>();
|
|
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions
|
|
{
|
|
SkipValidation = true,
|
|
}))
|
|
{
|
|
WriteDocument(writer, document);
|
|
}
|
|
|
|
var hash = SHA256.HashData(buffer.WrittenSpan);
|
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
|
}
|
|
|
|
private static void WriteDocument(Utf8JsonWriter writer, PolicyDocument document)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("version", document.Version);
|
|
|
|
if (!document.Metadata.IsEmpty)
|
|
{
|
|
writer.WritePropertyName("metadata");
|
|
writer.WriteStartObject();
|
|
foreach (var pair in document.Metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal))
|
|
{
|
|
writer.WriteString(pair.Key, pair.Value);
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WritePropertyName("rules");
|
|
writer.WriteStartArray();
|
|
foreach (var rule in document.Rules)
|
|
{
|
|
WriteRule(writer, rule);
|
|
}
|
|
writer.WriteEndArray();
|
|
|
|
if (!document.Exceptions.Effects.IsDefaultOrEmpty || !document.Exceptions.RoutingTemplates.IsDefaultOrEmpty)
|
|
{
|
|
writer.WritePropertyName("exceptions");
|
|
writer.WriteStartObject();
|
|
|
|
if (!document.Exceptions.Effects.IsDefaultOrEmpty)
|
|
{
|
|
writer.WritePropertyName("effects");
|
|
writer.WriteStartArray();
|
|
foreach (var effect in document.Exceptions.Effects
|
|
.OrderBy(static e => e.Id, StringComparer.Ordinal))
|
|
{
|
|
WriteExceptionEffect(writer, effect);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
if (!document.Exceptions.RoutingTemplates.IsDefaultOrEmpty)
|
|
{
|
|
writer.WritePropertyName("routingTemplates");
|
|
writer.WriteStartArray();
|
|
foreach (var template in document.Exceptions.RoutingTemplates
|
|
.OrderBy(static t => t.Id, StringComparer.Ordinal))
|
|
{
|
|
WriteExceptionRoutingTemplate(writer, template);
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
writer.Flush();
|
|
}
|
|
|
|
private static void WriteRule(Utf8JsonWriter writer, PolicyRule rule)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("name", rule.Name);
|
|
|
|
if (!string.IsNullOrWhiteSpace(rule.Identifier))
|
|
{
|
|
writer.WriteString("id", rule.Identifier);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(rule.Description))
|
|
{
|
|
writer.WriteString("description", rule.Description);
|
|
}
|
|
|
|
WriteMetadata(writer, rule.Metadata);
|
|
WriteSeverities(writer, rule.Severities);
|
|
WriteStringArray(writer, "environments", rule.Environments);
|
|
WriteStringArray(writer, "sources", rule.Sources);
|
|
WriteStringArray(writer, "vendors", rule.Vendors);
|
|
WriteStringArray(writer, "licenses", rule.Licenses);
|
|
WriteStringArray(writer, "tags", rule.Tags);
|
|
|
|
if (!rule.Match.IsEmpty)
|
|
{
|
|
writer.WritePropertyName("match");
|
|
writer.WriteStartObject();
|
|
WriteStringArray(writer, "images", rule.Match.Images);
|
|
WriteStringArray(writer, "repositories", rule.Match.Repositories);
|
|
WriteStringArray(writer, "packages", rule.Match.Packages);
|
|
WriteStringArray(writer, "purls", rule.Match.Purls);
|
|
WriteStringArray(writer, "cves", rule.Match.Cves);
|
|
WriteStringArray(writer, "paths", rule.Match.Paths);
|
|
WriteStringArray(writer, "layerDigests", rule.Match.LayerDigests);
|
|
WriteStringArray(writer, "usedByEntrypoint", rule.Match.UsedByEntrypoint);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
WriteAction(writer, rule.Action);
|
|
|
|
if (rule.Expires is DateTimeOffset expires)
|
|
{
|
|
writer.WriteString("expires", expires.ToUniversalTime().ToString("O"));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(rule.Justification))
|
|
{
|
|
writer.WriteString("justification", rule.Justification);
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteAction(Utf8JsonWriter writer, PolicyAction action)
|
|
{
|
|
writer.WritePropertyName("action");
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", action.Type.ToString().ToLowerInvariant());
|
|
|
|
if (action.Quiet)
|
|
{
|
|
writer.WriteBoolean("quiet", true);
|
|
}
|
|
|
|
if (action.Ignore is { } ignore)
|
|
{
|
|
if (ignore.Until is DateTimeOffset until)
|
|
{
|
|
writer.WriteString("until", until.ToUniversalTime().ToString("O"));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(ignore.Justification))
|
|
{
|
|
writer.WriteString("justification", ignore.Justification);
|
|
}
|
|
}
|
|
|
|
if (action.Escalate is { } escalate)
|
|
{
|
|
if (escalate.MinimumSeverity is { } severity)
|
|
{
|
|
writer.WriteString("severity", severity.ToString());
|
|
}
|
|
|
|
if (escalate.RequireKev)
|
|
{
|
|
writer.WriteBoolean("kev", true);
|
|
}
|
|
|
|
if (escalate.MinimumEpss is double epss)
|
|
{
|
|
writer.WriteNumber("epss", epss);
|
|
}
|
|
}
|
|
|
|
if (action.RequireVex is { } requireVex)
|
|
{
|
|
WriteStringArray(writer, "vendors", requireVex.Vendors);
|
|
WriteStringArray(writer, "justifications", requireVex.Justifications);
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteMetadata(Utf8JsonWriter writer, ImmutableDictionary<string, string> metadata)
|
|
{
|
|
if (metadata.IsEmpty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
writer.WritePropertyName("metadata");
|
|
writer.WriteStartObject();
|
|
foreach (var pair in metadata.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal))
|
|
{
|
|
writer.WriteString(pair.Key, pair.Value);
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteSeverities(Utf8JsonWriter writer, ImmutableArray<PolicySeverity> severities)
|
|
{
|
|
if (severities.IsDefaultOrEmpty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
writer.WritePropertyName("severity");
|
|
writer.WriteStartArray();
|
|
foreach (var severity in severities)
|
|
{
|
|
writer.WriteStringValue(severity.ToString());
|
|
}
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteStringArray(Utf8JsonWriter writer, string propertyName, ImmutableArray<string> values)
|
|
{
|
|
if (values.IsDefaultOrEmpty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
writer.WritePropertyName(propertyName);
|
|
writer.WriteStartArray();
|
|
foreach (var value in values)
|
|
{
|
|
writer.WriteStringValue(value);
|
|
}
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteExceptionEffect(Utf8JsonWriter writer, PolicyExceptionEffect effect)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("id", effect.Id);
|
|
|
|
if (!string.IsNullOrWhiteSpace(effect.Name))
|
|
{
|
|
writer.WriteString("name", effect.Name);
|
|
}
|
|
|
|
writer.WriteString("effect", effect.Effect.ToString().ToLowerInvariant());
|
|
|
|
if (effect.DowngradeSeverity is { } downgradeSeverity)
|
|
{
|
|
writer.WriteString("downgradeSeverity", downgradeSeverity.ToString());
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(effect.RequiredControlId))
|
|
{
|
|
writer.WriteString("requiredControlId", effect.RequiredControlId);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(effect.RoutingTemplate))
|
|
{
|
|
writer.WriteString("routingTemplate", effect.RoutingTemplate);
|
|
}
|
|
|
|
if (effect.MaxDurationDays is int maxDurationDays)
|
|
{
|
|
writer.WriteNumber("maxDurationDays", maxDurationDays);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(effect.Description))
|
|
{
|
|
writer.WriteString("description", effect.Description);
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void WriteExceptionRoutingTemplate(Utf8JsonWriter writer, PolicyExceptionRoutingTemplate template)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("id", template.Id);
|
|
writer.WriteString("authorityRouteId", template.AuthorityRouteId);
|
|
|
|
if (template.RequireMfa)
|
|
{
|
|
writer.WriteBoolean("requireMfa", true);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(template.Description))
|
|
{
|
|
writer.WriteString("description", template.Description);
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
}
|