tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

@@ -21,6 +21,9 @@ public static class DeterminizationEngineExtensions
// Add determinization library services
services.AddDeterminization();
// Add TimeProvider (default to system time if not already registered)
services.TryAddSingleton(TimeProvider.System);
// Add metrics
services.TryAddSingleton<DeterminizationGateMetrics>();
@@ -30,9 +33,18 @@ public static class DeterminizationEngineExtensions
// Add policy
services.TryAddSingleton<IDeterminizationPolicy, DeterminizationPolicy>();
// Add signal repository (default null implementation - register a real one to override)
services.TryAddSingleton<ISignalRepository>(NullSignalRepository.Instance);
// Add signal snapshot builder
services.TryAddSingleton<ISignalSnapshotBuilder, SignalSnapshotBuilder>();
// Add observation repository (default null implementation - register a real one to override)
services.TryAddSingleton<IObservationRepository>(NullObservationRepository.Instance);
// Add event publisher (default null implementation - register a real one to override)
services.TryAddSingleton<IEventPublisher>(NullEventPublisher.Instance);
// Add signal update subscription
services.TryAddSingleton<ISignalUpdateSubscription, SignalUpdateHandler>();

View File

@@ -19,6 +19,8 @@ using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Engine.Vex;
using StellaOps.Policy.Engine.WhatIfSimulation;
using StellaOps.Policy.Engine.Workers;
using StellaOps.Policy.Licensing;
using StellaOps.Policy.NtiaCompliance;
using StellaOps.Policy.Unknowns.Configuration;
using StellaOps.Policy.Unknowns.Services;
using StackExchange.Redis;
@@ -49,6 +51,19 @@ public static class PolicyEngineServiceCollectionExtensions
.BindConfiguration(UnknownBudgetOptions.SectionName);
services.TryAddSingleton<IUnknownBudgetService, UnknownBudgetService>();
services.AddOptions<LicenseComplianceOptions>()
.BindConfiguration(LicenseComplianceOptions.SectionName);
services.TryAddSingleton(_ => LicenseKnowledgeBase.LoadDefault());
services.TryAddSingleton<ILicensePolicyLoader, LicensePolicyLoader>();
services.TryAddSingleton<ILicenseComplianceEvaluator, LicenseComplianceEvaluator>();
services.TryAddSingleton<LicenseComplianceService>();
services.AddOptions<NtiaComplianceOptions>()
.BindConfiguration(NtiaComplianceOptions.SectionName);
services.TryAddSingleton<INtiaCompliancePolicyLoader, NtiaCompliancePolicyLoader>();
services.TryAddSingleton<INtiaComplianceValidator, NtiaBaselineValidator>();
services.TryAddSingleton<NtiaComplianceService>();
// Cache - uses IDistributedCacheFactory for transport flexibility
services.TryAddSingleton<IPolicyEvaluationCache, MessagingPolicyEvaluationCache>();

View File

@@ -85,8 +85,8 @@ internal static class EffectivePolicyEndpoints
private static IResult CreateEffectivePolicy(
HttpContext context,
[FromBody] CreateEffectivePolicyRequest request,
EffectivePolicyService policyService,
IEffectivePolicyAuditor auditor)
[FromServices] EffectivePolicyService policyService,
[FromServices] IEffectivePolicyAuditor auditor)
{
var scopeResult = RequireEffectiveWriteScope(context);
if (scopeResult is not null)
@@ -120,7 +120,7 @@ internal static class EffectivePolicyEndpoints
private static IResult GetEffectivePolicy(
HttpContext context,
[FromRoute] string effectivePolicyId,
EffectivePolicyService policyService)
[FromServices] EffectivePolicyService policyService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
@@ -144,8 +144,8 @@ internal static class EffectivePolicyEndpoints
HttpContext context,
[FromRoute] string effectivePolicyId,
[FromBody] UpdateEffectivePolicyRequest request,
EffectivePolicyService policyService,
IEffectivePolicyAuditor auditor)
[FromServices] EffectivePolicyService policyService,
[FromServices] IEffectivePolicyAuditor auditor)
{
var scopeResult = RequireEffectiveWriteScope(context);
if (scopeResult is not null)
@@ -178,8 +178,8 @@ internal static class EffectivePolicyEndpoints
private static IResult DeleteEffectivePolicy(
HttpContext context,
[FromRoute] string effectivePolicyId,
EffectivePolicyService policyService,
IEffectivePolicyAuditor auditor)
[FromServices] EffectivePolicyService policyService,
[FromServices] IEffectivePolicyAuditor auditor)
{
var scopeResult = RequireEffectiveWriteScope(context);
if (scopeResult is not null)
@@ -232,8 +232,8 @@ internal static class EffectivePolicyEndpoints
private static IResult AttachScope(
HttpContext context,
[FromBody] AttachAuthorityScopeRequest request,
EffectivePolicyService policyService,
IEffectivePolicyAuditor auditor)
[FromServices] EffectivePolicyService policyService,
[FromServices] IEffectivePolicyAuditor auditor)
{
var scopeResult = RequireEffectiveWriteScope(context);
if (scopeResult is not null)
@@ -268,8 +268,8 @@ internal static class EffectivePolicyEndpoints
private static IResult DetachScope(
HttpContext context,
[FromRoute] string attachmentId,
EffectivePolicyService policyService,
IEffectivePolicyAuditor auditor)
[FromServices] EffectivePolicyService policyService,
[FromServices] IEffectivePolicyAuditor auditor)
{
var scopeResult = RequireEffectiveWriteScope(context);
if (scopeResult is not null)
@@ -294,7 +294,7 @@ internal static class EffectivePolicyEndpoints
private static IResult GetPolicyScopeAttachments(
HttpContext context,
[FromRoute] string effectivePolicyId,
EffectivePolicyService policyService)
[FromServices] EffectivePolicyService policyService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)
@@ -311,7 +311,7 @@ internal static class EffectivePolicyEndpoints
HttpContext context,
[FromQuery] string subject,
[FromQuery] string? tenantId,
EffectivePolicyService policyService)
[FromServices] EffectivePolicyService policyService)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
if (scopeResult is not null)

View File

@@ -59,8 +59,8 @@ internal static class PolicyPackEndpoints
private static async Task<IResult> CreatePack(
HttpContext context,
[FromBody] CreatePolicyPackRequest request,
IPolicyPackRepository repository,
IGuidProvider guidProvider,
[FromServices] IPolicyPackRepository repository,
[FromServices] IGuidProvider guidProvider,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
@@ -90,7 +90,7 @@ internal static class PolicyPackEndpoints
private static async Task<IResult> ListPacks(
HttpContext context,
IPolicyPackRepository repository,
[FromServices] IPolicyPackRepository repository,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead);
@@ -108,8 +108,8 @@ internal static class PolicyPackEndpoints
HttpContext context,
[FromRoute] string packId,
[FromBody] CreatePolicyRevisionRequest request,
IPolicyPackRepository repository,
IPolicyActivationSettings activationSettings,
[FromServices] IPolicyPackRepository repository,
[FromServices] IPolicyActivationSettings activationSettings,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
@@ -157,9 +157,9 @@ internal static class PolicyPackEndpoints
[FromRoute] string packId,
[FromRoute] int version,
[FromBody] ActivatePolicyRevisionRequest request,
IPolicyPackRepository repository,
IPolicyActivationAuditor auditor,
TimeProvider timeProvider,
[FromServices] IPolicyPackRepository repository,
[FromServices] IPolicyActivationAuditor auditor,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyActivate);

View File

@@ -145,9 +145,9 @@ internal static class ProfileExportEndpoints
private static IResult ImportProfiles(
HttpContext context,
[FromBody] ImportProfilesRequest request,
RiskProfileConfigurationService profileService,
ProfileExportService exportService,
ICryptoHash cryptoHash)
[FromServices] RiskProfileConfigurationService profileService,
[FromServices] ProfileExportService exportService,
[FromServices] ICryptoHash cryptoHash)
{
var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit);
if (scopeResult is not null)

View File

@@ -5,6 +5,9 @@ using System.Linq;
using StellaOps.Policy;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Licensing;
using StellaOps.Policy.NtiaCompliance;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.PolicyDsl;
using StellaOps.Signals.EvidenceWeightedScore;
@@ -96,16 +99,21 @@ internal sealed record PolicyEvaluationVexStatement(
internal sealed record PolicyEvaluationSbom(
ImmutableHashSet<string> Tags,
ImmutableArray<PolicyEvaluationComponent> Components)
ImmutableArray<PolicyEvaluationComponent> Components,
LicenseComplianceReport? LicenseReport = null)
{
public ParsedSbom? Parsed { get; init; }
public NtiaComplianceReport? NtiaReport { get; init; }
public PolicyEvaluationSbom(ImmutableHashSet<string> Tags)
: this(Tags, ImmutableArray<PolicyEvaluationComponent>.Empty)
: this(Tags, ImmutableArray<PolicyEvaluationComponent>.Empty, null)
{
}
public static readonly PolicyEvaluationSbom Empty = new(
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableArray<PolicyEvaluationComponent>.Empty);
ImmutableArray<PolicyEvaluationComponent>.Empty,
null);
public bool HasTag(string tag) => Tags.Contains(tag);
}

View File

@@ -4,6 +4,8 @@ using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using StellaOps.PolicyDsl;
using StellaOps.Policy.Licensing;
using StellaOps.Policy.NtiaCompliance;
using StellaOps.Signals.EvidenceWeightedScore;
namespace StellaOps.Policy.Engine.Evaluation;
@@ -109,6 +111,31 @@ internal sealed class PolicyExpressionEvaluator
return sbom.Get(member.Member);
}
if (raw is LicenseScope licenseScope)
{
return licenseScope.Get(member.Member);
}
if (raw is NtiaScope ntiaScope)
{
return ntiaScope.Get(member.Member);
}
if (raw is LicenseFindingScope findingScope)
{
return findingScope.Get(member.Member);
}
if (raw is LicenseUsageScope usageScope)
{
return usageScope.Get(member.Member);
}
if (raw is LicenseConflictScope conflictScope)
{
return conflictScope.Get(member.Member);
}
if (raw is ReachabilityScope reachability)
{
return reachability.Get(member.Member);
@@ -541,6 +568,33 @@ internal sealed class PolicyExpressionEvaluator
.ToImmutableArray());
}
if (member.Equals("license", StringComparison.OrdinalIgnoreCase))
{
return new EvaluationValue(new LicenseScope(sbom.LicenseReport));
}
if (member.Equals("license_status", StringComparison.OrdinalIgnoreCase))
{
var status = sbom.LicenseReport?.OverallStatus.ToString().ToLowerInvariant() ?? "unknown";
return new EvaluationValue(status);
}
if (member.Equals("ntia", StringComparison.OrdinalIgnoreCase))
{
return new EvaluationValue(new NtiaScope(sbom.NtiaReport));
}
if (member.Equals("ntia_status", StringComparison.OrdinalIgnoreCase))
{
var status = sbom.NtiaReport?.OverallStatus.ToString().ToLowerInvariant() ?? "unknown";
return new EvaluationValue(status);
}
if (member.Equals("ntia_score", StringComparison.OrdinalIgnoreCase))
{
return new EvaluationValue(sbom.NtiaReport?.ComplianceScore);
}
return EvaluationValue.Null;
}
@@ -594,6 +648,187 @@ internal sealed class PolicyExpressionEvaluator
}
}
private sealed class LicenseScope
{
private readonly LicenseComplianceReport? report;
public LicenseScope(LicenseComplianceReport? report)
{
this.report = report;
}
public EvaluationValue Get(string member)
{
if (report is null)
{
return EvaluationValue.Null;
}
return member.ToLowerInvariant() switch
{
"status" => new EvaluationValue(report.OverallStatus.ToString().ToLowerInvariant()),
"findings" => new EvaluationValue(report.Findings
.Select(finding => (object?)new LicenseFindingScope(finding))
.ToImmutableArray()),
"conflicts" => new EvaluationValue(report.Conflicts
.Select(conflict => (object?)new LicenseConflictScope(conflict))
.ToImmutableArray()),
"inventory" => new EvaluationValue(report.Inventory.Licenses
.Select(usage => (object?)new LicenseUsageScope(usage))
.ToImmutableArray()),
_ => EvaluationValue.Null
};
}
}
private sealed class NtiaScope
{
private readonly NtiaComplianceReport? report;
public NtiaScope(NtiaComplianceReport? report)
{
this.report = report;
}
public EvaluationValue Get(string member)
{
if (report is null)
{
return EvaluationValue.Null;
}
return member.ToLowerInvariant() switch
{
"status" => new EvaluationValue(report.OverallStatus.ToString().ToLowerInvariant()),
"score" => new EvaluationValue(report.ComplianceScore),
"supplier_status" or "supplierstatus" => new EvaluationValue(report.SupplierStatus.ToString().ToLowerInvariant()),
"elements" => new EvaluationValue(report.ElementStatuses
.Select(status => (object?)new NtiaElementStatusScope(status))
.ToImmutableArray()),
"findings" => new EvaluationValue(report.Findings
.Select(finding => (object?)new NtiaFindingScope(finding))
.ToImmutableArray()),
_ => EvaluationValue.Null
};
}
}
private sealed class NtiaElementStatusScope
{
private readonly NtiaElementStatus status;
public NtiaElementStatusScope(NtiaElementStatus status)
{
this.status = status;
}
public EvaluationValue Get(string member)
{
return member.ToLowerInvariant() switch
{
"element" => new EvaluationValue(status.Element.ToString().ToLowerInvariant()),
"present" => new EvaluationValue(status.Present),
"valid" => new EvaluationValue(status.Valid),
"covered" => new EvaluationValue(status.ComponentsCovered),
"missing" => new EvaluationValue(status.ComponentsMissing),
"notes" => new EvaluationValue(status.Notes),
_ => EvaluationValue.Null
};
}
}
private sealed class NtiaFindingScope
{
private readonly NtiaFinding finding;
public NtiaFindingScope(NtiaFinding finding)
{
this.finding = finding;
}
public EvaluationValue Get(string member)
{
return member.ToLowerInvariant() switch
{
"type" => new EvaluationValue(finding.Type.ToString().ToLowerInvariant()),
"element" => new EvaluationValue(finding.Element?.ToString().ToLowerInvariant()),
"component" => new EvaluationValue(finding.Component),
"supplier" => new EvaluationValue(finding.Supplier),
"count" => new EvaluationValue(finding.Count),
"message" => new EvaluationValue(finding.Message),
_ => EvaluationValue.Null
};
}
}
private sealed class LicenseFindingScope
{
private readonly LicenseFinding finding;
public LicenseFindingScope(LicenseFinding finding)
{
this.finding = finding;
}
public EvaluationValue Get(string member)
{
return member.ToLowerInvariant() switch
{
"type" => new EvaluationValue(finding.Type.ToString().ToLowerInvariant()),
"license" => new EvaluationValue(finding.LicenseId),
"component" => new EvaluationValue(finding.ComponentName),
"purl" => new EvaluationValue(finding.ComponentPurl),
"category" => new EvaluationValue(finding.Category.ToString().ToLowerInvariant()),
"message" => new EvaluationValue(finding.Message),
_ => EvaluationValue.Null
};
}
}
private sealed class LicenseUsageScope
{
private readonly LicenseUsage usage;
public LicenseUsageScope(LicenseUsage usage)
{
this.usage = usage;
}
public EvaluationValue Get(string member)
{
return member.ToLowerInvariant() switch
{
"license" => new EvaluationValue(usage.LicenseId),
"category" => new EvaluationValue(usage.Category.ToString().ToLowerInvariant()),
"count" => new EvaluationValue(usage.Count),
"components" => new EvaluationValue(usage.Components.Select(value => (object?)value).ToImmutableArray()),
_ => EvaluationValue.Null
};
}
}
private sealed class LicenseConflictScope
{
private readonly LicenseConflict conflict;
public LicenseConflictScope(LicenseConflict conflict)
{
this.conflict = conflict;
}
public EvaluationValue Get(string member)
{
return member.ToLowerInvariant() switch
{
"component" => new EvaluationValue(conflict.ComponentName),
"purl" => new EvaluationValue(conflict.ComponentPurl),
"licenses" => new EvaluationValue(conflict.LicenseIds.Select(value => (object?)value).ToImmutableArray()),
"reason" => new EvaluationValue(conflict.Reason),
_ => EvaluationValue.Null
};
}
}
private sealed class ComponentScope
{
private readonly PolicyEvaluationComponent component;

View File

@@ -161,6 +161,18 @@ public interface ISignalRepository
Task<IReadOnlyList<Signal>> GetSignalsAsync(string subjectKey, CancellationToken ct = default);
}
/// <summary>
/// Null object pattern implementation that returns empty signals.
/// Used as default when no real signal repository is configured.
/// </summary>
public sealed class NullSignalRepository : ISignalRepository
{
public static readonly NullSignalRepository Instance = new();
public Task<IReadOnlyList<Signal>> GetSignalsAsync(string subjectKey, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Signal>>(Array.Empty<Signal>());
}
/// <summary>
/// Represents a signal retrieved from storage.
/// </summary>

View File

@@ -0,0 +1,12 @@
using StellaOps.Policy.Licensing;
namespace StellaOps.Policy.Engine.Options;
public sealed record LicenseComplianceOptions
{
public const string SectionName = "licenseCompliance";
public bool Enabled { get; init; } = true;
public string? PolicyPath { get; init; }
public LicensePolicy? Policy { get; init; }
}

View File

@@ -0,0 +1,13 @@
using StellaOps.Policy.NtiaCompliance;
namespace StellaOps.Policy.Engine.Options;
public sealed record NtiaComplianceOptions
{
public const string SectionName = "ntiaCompliance";
public bool Enabled { get; init; } = false;
public bool EnforceGate { get; init; } = false;
public string? PolicyPath { get; init; }
public NtiaCompliancePolicy? Policy { get; init; }
}

View File

@@ -0,0 +1,107 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Engine.Evaluation;
using StellaOps.Policy.Engine.Options;
using StellaOps.Policy.Licensing;
namespace StellaOps.Policy.Engine.Services;
internal sealed class LicenseComplianceService
{
private readonly ILicenseComplianceEvaluator _evaluator;
private readonly ILicensePolicyLoader _policyLoader;
private readonly LicenseComplianceOptions _options;
private readonly ILogger<LicenseComplianceService> _logger;
private readonly Lazy<LicensePolicy> _policy;
public LicenseComplianceService(
ILicenseComplianceEvaluator evaluator,
ILicensePolicyLoader policyLoader,
IOptions<LicenseComplianceOptions> options,
ILogger<LicenseComplianceService> logger)
{
_evaluator = evaluator ?? throw new ArgumentNullException(nameof(evaluator));
_policyLoader = policyLoader ?? throw new ArgumentNullException(nameof(policyLoader));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_policy = new Lazy<LicensePolicy>(ResolvePolicy);
}
public async Task<LicenseComplianceReport?> EvaluateAsync(
PolicyEvaluationSbom sbom,
CancellationToken ct)
{
if (!_options.Enabled)
{
return null;
}
var components = sbom.Components
.Select(MapComponent)
.ToList();
try
{
var report = await _evaluator.EvaluateAsync(components, _policy.Value, ct)
.ConfigureAwait(false);
return report;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "License compliance evaluation failed; proceeding without report.");
return null;
}
}
private LicensePolicy ResolvePolicy()
{
if (_options.Policy is not null)
{
return _options.Policy;
}
if (!string.IsNullOrWhiteSpace(_options.PolicyPath))
{
return _policyLoader.Load(_options.PolicyPath);
}
return LicensePolicyDefaults.Default;
}
private static LicenseComponent MapComponent(PolicyEvaluationComponent component)
{
var expression = GetMetadata(component, "license_expression")
?? GetMetadata(component, "licenseexpression")
?? GetMetadata(component, "spdx_license_expression")
?? GetMetadata(component, "license")
?? GetMetadata(component, "licenses");
var licenses = ImmutableArray<string>.Empty;
if (!string.IsNullOrWhiteSpace(expression) && expression.Contains(','))
{
licenses = expression
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(value => value.Trim())
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToImmutableArray();
expression = null;
}
return new LicenseComponent
{
Name = component.Name,
Version = component.Version,
Purl = component.Purl,
LicenseExpression = expression,
Licenses = licenses,
Metadata = component.Metadata
};
}
private static string? GetMetadata(PolicyEvaluationComponent component, string key)
{
return component.Metadata.TryGetValue(key, out var value) ? value : null;
}
}

View File

@@ -0,0 +1,82 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Engine.Evaluation;
using StellaOps.Policy.Engine.Options;
using StellaOps.Policy.NtiaCompliance;
namespace StellaOps.Policy.Engine.Services;
internal sealed class NtiaComplianceService
{
private readonly INtiaComplianceValidator _validator;
private readonly INtiaCompliancePolicyLoader _policyLoader;
private readonly NtiaComplianceOptions _options;
private readonly ILogger<NtiaComplianceService> _logger;
private readonly Lazy<NtiaCompliancePolicy> _policy;
public NtiaComplianceService(
INtiaComplianceValidator validator,
INtiaCompliancePolicyLoader policyLoader,
IOptions<NtiaComplianceOptions> options,
ILogger<NtiaComplianceService> logger)
{
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
_policyLoader = policyLoader ?? throw new ArgumentNullException(nameof(policyLoader));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_policy = new Lazy<NtiaCompliancePolicy>(ResolvePolicy);
}
public bool EnforceGate => _options.EnforceGate;
public async Task<NtiaComplianceReport?> EvaluateAsync(
PolicyEvaluationSbom sbom,
CancellationToken ct)
{
if (!_options.Enabled)
{
return null;
}
if (sbom.Parsed is null)
{
_logger.LogWarning("NTIA compliance evaluation skipped; ParsedSbom is missing.");
return new NtiaComplianceReport
{
OverallStatus = NtiaComplianceStatus.Unknown,
ComplianceScore = 0.0
};
}
try
{
return await _validator.ValidateAsync(sbom.Parsed, _policy.Value, ct)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "NTIA compliance evaluation failed; proceeding without report.");
return new NtiaComplianceReport
{
OverallStatus = NtiaComplianceStatus.Unknown,
ComplianceScore = 0.0
};
}
}
private NtiaCompliancePolicy ResolvePolicy()
{
if (_options.Policy is not null)
{
return _options.Policy;
}
if (!string.IsNullOrWhiteSpace(_options.PolicyPath))
{
return _policyLoader.Load(_options.PolicyPath);
}
return new NtiaCompliancePolicy();
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
@@ -9,6 +10,8 @@ using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Engine.Caching;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Evaluation;
using StellaOps.Policy.Licensing;
using StellaOps.Policy.NtiaCompliance;
using StellaOps.Policy.Engine.Telemetry;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Unknowns.Models;
@@ -68,6 +71,8 @@ internal sealed class PolicyRuntimeEvaluationService
private readonly PolicyEvaluator _evaluator;
private readonly ReachabilityFacts.ReachabilityFactsJoiningService? _reachabilityFacts;
private readonly Signals.Entropy.EntropyPenaltyCalculator _entropy;
private readonly LicenseComplianceService? _licenseCompliance;
private readonly NtiaComplianceService? _ntiaCompliance;
private readonly TimeProvider _timeProvider;
private readonly ILogger<PolicyRuntimeEvaluationService> _logger;
@@ -83,6 +88,8 @@ internal sealed class PolicyRuntimeEvaluationService
PolicyEvaluator evaluator,
ReachabilityFacts.ReachabilityFactsJoiningService? reachabilityFacts,
Signals.Entropy.EntropyPenaltyCalculator entropy,
LicenseComplianceService? licenseCompliance,
NtiaComplianceService? ntiaCompliance,
TimeProvider timeProvider,
ILogger<PolicyRuntimeEvaluationService> logger)
{
@@ -91,6 +98,8 @@ internal sealed class PolicyRuntimeEvaluationService
_evaluator = evaluator ?? throw new ArgumentNullException(nameof(evaluator));
_reachabilityFacts = reachabilityFacts;
_entropy = entropy ?? throw new ArgumentNullException(nameof(entropy));
_licenseCompliance = licenseCompliance;
_ntiaCompliance = ntiaCompliance;
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -130,6 +139,34 @@ internal sealed class PolicyRuntimeEvaluationService
}
// Compute deterministic cache key
if (_licenseCompliance is not null)
{
var licenseReport = await _licenseCompliance
.EvaluateAsync(effectiveRequest.Sbom, cancellationToken)
.ConfigureAwait(false);
if (licenseReport is not null)
{
effectiveRequest = effectiveRequest with
{
Sbom = effectiveRequest.Sbom with { LicenseReport = licenseReport }
};
}
}
if (_ntiaCompliance is not null)
{
var ntiaReport = await _ntiaCompliance
.EvaluateAsync(effectiveRequest.Sbom, cancellationToken)
.ConfigureAwait(false);
if (ntiaReport is not null)
{
effectiveRequest = effectiveRequest with
{
Sbom = effectiveRequest.Sbom with { NtiaReport = ntiaReport }
};
}
}
var subjectDigest = ComputeSubjectDigest(effectiveRequest.TenantId, effectiveRequest.SubjectPurl, effectiveRequest.AdvisoryId);
var contextDigest = ComputeContextDigest(effectiveRequest);
var cacheKey = PolicyEvaluationCacheKey.Create(bundle.Digest, subjectDigest, contextDigest);
@@ -188,6 +225,8 @@ internal sealed class PolicyRuntimeEvaluationService
var evalRequest = new Evaluation.PolicyEvaluationRequest(document, context);
var result = _evaluator.Evaluate(evalRequest);
result = ApplyLicenseCompliance(result, effectiveRequest.Sbom.LicenseReport);
result = ApplyNtiaCompliance(result, effectiveRequest.Sbom.NtiaReport, _ntiaCompliance?.EnforceGate ?? false);
var correlationId = ComputeCorrelationId(bundle.Digest, subjectDigest, contextDigest);
var expiresAt = evaluationTimestamp.AddMinutes(30);
@@ -284,8 +323,56 @@ internal sealed class PolicyRuntimeEvaluationService
? requests
: await EnrichReachabilityBatchAsync(requests, cancellationToken).ConfigureAwait(false);
var licenseHydratedRequests = hydratedRequests;
if (_licenseCompliance is not null)
{
var updated = new List<RuntimeEvaluationRequest>(hydratedRequests.Count);
foreach (var request in hydratedRequests)
{
var report = await _licenseCompliance.EvaluateAsync(request.Sbom, cancellationToken)
.ConfigureAwait(false);
if (report is not null)
{
updated.Add(request with
{
Sbom = request.Sbom with { LicenseReport = report }
});
}
else
{
updated.Add(request);
}
}
licenseHydratedRequests = updated;
}
var complianceHydratedRequests = licenseHydratedRequests;
if (_ntiaCompliance is not null)
{
var updated = new List<RuntimeEvaluationRequest>(licenseHydratedRequests.Count);
foreach (var request in licenseHydratedRequests)
{
var report = await _ntiaCompliance.EvaluateAsync(request.Sbom, cancellationToken)
.ConfigureAwait(false);
if (report is not null)
{
updated.Add(request with
{
Sbom = request.Sbom with { NtiaReport = report }
});
}
else
{
updated.Add(request);
}
}
complianceHydratedRequests = updated;
}
// Group by pack/version for bundle loading efficiency
var groups = hydratedRequests.GroupBy(r => (r.PackId, r.Version));
var groups = complianceHydratedRequests.GroupBy(r => (r.PackId, r.Version));
foreach (var group in groups)
{
@@ -374,6 +461,8 @@ internal sealed class PolicyRuntimeEvaluationService
var evalRequest = new Evaluation.PolicyEvaluationRequest(document, context);
var result = _evaluator.Evaluate(evalRequest);
result = ApplyLicenseCompliance(result, request.Sbom.LicenseReport);
result = ApplyNtiaCompliance(result, request.Sbom.NtiaReport, _ntiaCompliance?.EnforceGate ?? false);
var correlationId = ComputeCorrelationId(bundle.Digest, key.SubjectDigest, key.ContextDigest);
var expiresAt = evaluationTimestamp.AddMinutes(30);
@@ -519,6 +608,27 @@ internal sealed class PolicyRuntimeEvaluationService
.ToArray(),
sbomTags = request.Sbom.Tags.OrderBy(t => t).ToArray(),
sbomComponentCount = request.Sbom.Components.IsDefaultOrEmpty ? 0 : request.Sbom.Components.Length,
license = request.Sbom.LicenseReport is null ? null : new
{
status = request.Sbom.LicenseReport.OverallStatus.ToString().ToLowerInvariant(),
findingCount = request.Sbom.LicenseReport.Findings.Length,
conflictCount = request.Sbom.LicenseReport.Conflicts.Length,
licenseIds = request.Sbom.LicenseReport.Inventory.Licenses
.Select(usage => usage.LicenseId)
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
.ToArray()
},
ntia = request.Sbom.NtiaReport is null ? null : new
{
status = request.Sbom.NtiaReport.OverallStatus.ToString().ToLowerInvariant(),
score = request.Sbom.NtiaReport.ComplianceScore,
supplierStatus = request.Sbom.NtiaReport.SupplierStatus.ToString().ToLowerInvariant(),
missingElements = request.Sbom.NtiaReport.ElementStatuses
.Where(status => !status.Valid)
.Select(status => status.Element.ToString())
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
.ToArray()
},
exceptionCount = request.Exceptions.Instances.Length,
reachability = new
{
@@ -707,4 +817,81 @@ internal sealed class PolicyRuntimeEvaluationService
return reachability;
}
private static StellaOps.Policy.Engine.Evaluation.PolicyEvaluationResult ApplyLicenseCompliance(
StellaOps.Policy.Engine.Evaluation.PolicyEvaluationResult result,
LicenseComplianceReport? report)
{
if (report is null)
{
return result;
}
var annotations = result.Annotations.ToBuilder();
annotations["license.status"] = report.OverallStatus.ToString().ToLowerInvariant();
annotations["license.findings"] = report.Findings.Length.ToString(CultureInfo.InvariantCulture);
annotations["license.conflicts"] = report.Conflicts.Length.ToString(CultureInfo.InvariantCulture);
var warnings = result.Warnings;
if (report.OverallStatus == LicenseComplianceStatus.Fail)
{
warnings = warnings.Add("License compliance failed.");
return result with
{
Status = "blocked",
Annotations = annotations.ToImmutable(),
Warnings = warnings
};
}
if (report.OverallStatus == LicenseComplianceStatus.Warn)
{
warnings = warnings.Add("License compliance has warnings.");
}
return result with
{
Annotations = annotations.ToImmutable(),
Warnings = warnings
};
}
private static StellaOps.Policy.Engine.Evaluation.PolicyEvaluationResult ApplyNtiaCompliance(
StellaOps.Policy.Engine.Evaluation.PolicyEvaluationResult result,
NtiaComplianceReport? report,
bool enforceGate)
{
if (report is null)
{
return result;
}
var annotations = result.Annotations.ToBuilder();
annotations["ntia.status"] = report.OverallStatus.ToString().ToLowerInvariant();
annotations["ntia.score"] = report.ComplianceScore.ToString("0.00", CultureInfo.InvariantCulture);
annotations["ntia.supplier_status"] = report.SupplierStatus.ToString().ToLowerInvariant();
var warnings = result.Warnings;
if (report.OverallStatus == NtiaComplianceStatus.Fail && enforceGate)
{
warnings = warnings.Add("NTIA compliance failed.");
return result with
{
Status = "blocked",
Annotations = annotations.ToImmutable(),
Warnings = warnings
};
}
if (report.OverallStatus is NtiaComplianceStatus.Fail or NtiaComplianceStatus.Warn)
{
warnings = warnings.Add("NTIA compliance requires review.");
}
return result with
{
Annotations = annotations.ToImmutable(),
Warnings = warnings
};
}
}

View File

@@ -46,6 +46,7 @@
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.ProofSpine/StellaOps.Scanner.ProofSpine.csproj" />
<ProjectReference Include="../../Signals/StellaOps.Signals/StellaOps.Signals.csproj" />
<ProjectReference Include="../../SbomService/__Libraries/StellaOps.SbomService.Persistence/StellaOps.SbomService.Persistence.csproj" />
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.SbomIntegration/StellaOps.Concelier.SbomIntegration.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Policy.Engine.Tests" />

View File

@@ -222,6 +222,19 @@ public interface IObservationRepository
CancellationToken ct = default);
}
/// <summary>
/// Null object pattern implementation for IObservationRepository.
/// Returns empty results. Register a real implementation to override.
/// </summary>
public sealed class NullObservationRepository : IObservationRepository
{
public static readonly NullObservationRepository Instance = new();
public Task<IReadOnlyList<CveObservation>> FindByCveAndPurlAsync(
string cveId, string purl, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<CveObservation>>(Array.Empty<CveObservation>());
}
/// <summary>
/// Event publisher abstraction.
/// </summary>
@@ -234,6 +247,18 @@ public interface IEventPublisher
where TEvent : class;
}
/// <summary>
/// Null object pattern implementation for IEventPublisher.
/// Discards events silently. Register a real implementation to override.
/// </summary>
public sealed class NullEventPublisher : IEventPublisher
{
public static readonly NullEventPublisher Instance = new();
public Task PublishAsync<TEvent>(TEvent evt, CancellationToken ct = default)
where TEvent : class => Task.CompletedTask;
}
/// <summary>
/// CVE observation model.
/// </summary>

View File

@@ -1,7 +1,7 @@
# StellaOps.Policy.Engine Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
Source of truth: `docs/implplan/SPRINT_20260119_021_Policy_license_compliance.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
@@ -9,3 +9,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0440-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Engine. |
| AUDIT-0440-A | DOING | Revalidated 2026-01-07 (open findings). |
| AUDIT-HOTLIST-POLICY-ENGINE-0001 | DOING | Apply approved hotlist fixes and tests from audit tracker. |
| TASK-021-009 | BLOCKED | License compliance integrated into runtime evaluation; CLI overrides need API contract. |
| TASK-021-011 | DOING | Engine-level tests updated for license compliance gating; suite stability pending. |
| TASK-021-012 | DONE | Real SBOM integration tests added (npm-monorepo, alpine-busybox, python-venv, java-multi-license); filtered integration runs passed. |