tests fixes and sprints work
This commit is contained in:
@@ -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>();
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user