Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
40
src/Policy/__Libraries/StellaOps.Policy.Unknowns/AGENTS.md
Normal file
40
src/Policy/__Libraries/StellaOps.Policy.Unknowns/AGENTS.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# AGENTS.md - Policy Unknowns Library
|
||||
|
||||
## Purpose
|
||||
- Provide deterministic ranking for unknown findings using uncertainty, exploit pressure, decay, and containment signals.
|
||||
- Maintain stable, reproducible scoring and band assignment.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/policy/architecture.md
|
||||
- docs/product-advisories/archived/2025-12-21-moat-gap-closure/14-Dec-2025 - Triage and Unknowns Technical Reference.md
|
||||
|
||||
## Working Directory
|
||||
- src/Policy/__Libraries/StellaOps.Policy.Unknowns/
|
||||
|
||||
## Signal Sources
|
||||
|
||||
### BlastRadius
|
||||
- Source: Scanner/Signals module call graph analysis.
|
||||
- Dependents: count of packages in dependency tree.
|
||||
- NetFacing: reachability from network entrypoints (HTTP controllers, gRPC, etc).
|
||||
- Privilege: extracted from container config or runtime probes.
|
||||
|
||||
### ContainmentSignals
|
||||
- Source: runtime probes (eBPF, Seccomp profiles, container inspection).
|
||||
- Seccomp: profile enforcement status.
|
||||
- FileSystem: mount mode from container spec or /proc/mounts.
|
||||
- NetworkPolicy: Kubernetes NetworkPolicy or firewall rules.
|
||||
|
||||
### Data Flow
|
||||
1. Scanner generates BlastRadius during SBOM or call graph analysis.
|
||||
2. Runtime probes collect ContainmentSignals.
|
||||
3. Signals are stored in policy.unknowns columns.
|
||||
4. UnknownRanker reads signals for scoring and explainability.
|
||||
|
||||
## Engineering Rules
|
||||
- Target net10.0 with preview features already enabled in repo.
|
||||
- Determinism: stable ordering, UTC timestamps, and decimal math for scoring.
|
||||
- No network dependencies inside ranking logic.
|
||||
@@ -0,0 +1,22 @@
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
namespace StellaOps.Policy.Unknowns.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for unknown budgets.
|
||||
/// </summary>
|
||||
public sealed class UnknownBudgetOptions
|
||||
{
|
||||
public const string SectionName = "UnknownBudgets";
|
||||
|
||||
/// <summary>
|
||||
/// Budget configurations keyed by environment name.
|
||||
/// </summary>
|
||||
public Dictionary<string, UnknownBudget> Budgets { get; set; } =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enforce budgets (false = warn only).
|
||||
/// </summary>
|
||||
public bool EnforceBudgets { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the dependency graph impact of an unknown package.
|
||||
/// Data sourced from scanner call graph analysis.
|
||||
/// </summary>
|
||||
public sealed record BlastRadius
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of packages that directly or transitively depend on this package.
|
||||
/// 0 indicates isolation.
|
||||
/// </summary>
|
||||
public int Dependents { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this package is reachable from network-facing entrypoints.
|
||||
/// True indicates higher risk.
|
||||
/// </summary>
|
||||
public bool NetFacing { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Privilege level under which this package typically runs.
|
||||
/// Expected values: root, user, none.
|
||||
/// </summary>
|
||||
public string? Privilege { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents runtime isolation and containment posture signals.
|
||||
/// Data sourced from runtime probes.
|
||||
/// </summary>
|
||||
public sealed record ContainmentSignals
|
||||
{
|
||||
/// <summary>
|
||||
/// Seccomp profile status: enforced, permissive, disabled, or null if unknown.
|
||||
/// </summary>
|
||||
public string? Seccomp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Filesystem mount mode: ro, rw, or null if unknown.
|
||||
/// </summary>
|
||||
public string? FileSystem { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Network policy status: isolated, restricted, open, or null if unknown.
|
||||
/// </summary>
|
||||
public string? NetworkPolicy { get; init; }
|
||||
}
|
||||
@@ -56,6 +56,24 @@ public sealed record Unknown
|
||||
/// <summary>Exploit pressure from KEV/EPSS/CVSS (0.0000 - 1.0000).</summary>
|
||||
public required decimal ExploitPressure { get; init; }
|
||||
|
||||
/// <summary>Reason code explaining why this entry is unknown.</summary>
|
||||
public required UnknownReasonCode ReasonCode { get; init; }
|
||||
|
||||
/// <summary>Human-readable remediation guidance for this unknown.</summary>
|
||||
public string? RemediationHint { get; init; }
|
||||
|
||||
/// <summary>References to evidence supporting the unknown classification.</summary>
|
||||
public IReadOnlyList<EvidenceRef> EvidenceRefs { get; init; } = [];
|
||||
|
||||
/// <summary>Assumptions applied during analysis.</summary>
|
||||
public IReadOnlyList<string> Assumptions { get; init; } = [];
|
||||
|
||||
/// <summary>Dependency impact signals for containment reduction.</summary>
|
||||
public BlastRadius? BlastRadius { get; init; }
|
||||
|
||||
/// <summary>Runtime containment posture signals.</summary>
|
||||
public ContainmentSignals? Containment { get; init; }
|
||||
|
||||
/// <summary>When this unknown was first detected.</summary>
|
||||
public required DateTimeOffset FirstSeenAt { get; init; }
|
||||
|
||||
@@ -75,6 +93,14 @@ public sealed record Unknown
|
||||
public required DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to evidence supporting unknown classification.
|
||||
/// </summary>
|
||||
public sealed record EvidenceRef(
|
||||
string Type,
|
||||
string Uri,
|
||||
string? Digest);
|
||||
|
||||
/// <summary>
|
||||
/// Summary counts of unknowns by band for dashboard display.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
namespace StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an unknown budget for a specific environment.
|
||||
/// Budgets define maximum acceptable unknown counts by reason code.
|
||||
/// </summary>
|
||||
public sealed record UnknownBudget
|
||||
{
|
||||
/// <summary>
|
||||
/// Environment name: "prod", "stage", "dev", or custom.
|
||||
/// </summary>
|
||||
public required string Environment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum total unknowns allowed across all reason codes.
|
||||
/// </summary>
|
||||
public int? TotalLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Per-reason-code limits. Missing codes inherit from TotalLimit.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<UnknownReasonCode, int> ReasonLimits { get; init; }
|
||||
= new Dictionary<UnknownReasonCode, int>();
|
||||
|
||||
/// <summary>
|
||||
/// Action when budget is exceeded.
|
||||
/// </summary>
|
||||
public BudgetAction Action { get; init; } = BudgetAction.Warn;
|
||||
|
||||
/// <summary>
|
||||
/// Custom message to display when budget is exceeded.
|
||||
/// </summary>
|
||||
public string? ExceededMessage { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when unknown budget is exceeded.
|
||||
/// </summary>
|
||||
public enum BudgetAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Log warning only, do not block.
|
||||
/// </summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>
|
||||
/// Block the operation (fail policy evaluation).
|
||||
/// </summary>
|
||||
Block,
|
||||
|
||||
/// <summary>
|
||||
/// Warn but allow if exception is applied.
|
||||
/// </summary>
|
||||
WarnUnlessException
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of checking unknowns against a budget.
|
||||
/// </summary>
|
||||
public sealed record BudgetCheckResult
|
||||
{
|
||||
public required bool IsWithinBudget { get; init; }
|
||||
public required BudgetAction RecommendedAction { get; init; }
|
||||
public required int TotalUnknowns { get; init; }
|
||||
public int? TotalLimit { get; init; }
|
||||
public IReadOnlyDictionary<UnknownReasonCode, BudgetViolation> Violations { get; init; }
|
||||
= new Dictionary<UnknownReasonCode, BudgetViolation>();
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details of a specific budget violation.
|
||||
/// </summary>
|
||||
public sealed record BudgetViolation(
|
||||
UnknownReasonCode ReasonCode,
|
||||
int Count,
|
||||
int Limit);
|
||||
|
||||
/// <summary>
|
||||
/// Summary of budget status for reporting and dashboards.
|
||||
/// </summary>
|
||||
public sealed record BudgetStatusSummary
|
||||
{
|
||||
public required string Environment { get; init; }
|
||||
public required int TotalUnknowns { get; init; }
|
||||
public int? TotalLimit { get; init; }
|
||||
public decimal PercentageUsed { get; init; }
|
||||
public bool IsExceeded { get; init; }
|
||||
public int ViolationCount { get; init; }
|
||||
public IReadOnlyDictionary<UnknownReasonCode, int> ByReasonCode { get; init; }
|
||||
= new Dictionary<UnknownReasonCode, int>();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical reason codes explaining why a component is marked as unknown.
|
||||
/// Each code maps to a specific remediation action.
|
||||
/// </summary>
|
||||
public enum UnknownReasonCode
|
||||
{
|
||||
/// <summary>
|
||||
/// U-RCH: Call path analysis is indeterminate.
|
||||
/// The reachability analyzer cannot confirm or deny exploitability.
|
||||
/// </summary>
|
||||
Reachability,
|
||||
|
||||
/// <summary>
|
||||
/// U-ID: Ambiguous package identity or missing digest.
|
||||
/// Cannot uniquely identify the component (e.g., missing PURL, no checksum).
|
||||
/// </summary>
|
||||
Identity,
|
||||
|
||||
/// <summary>
|
||||
/// U-PROV: Cannot map binary artifact to source repository.
|
||||
/// Provenance chain is broken or unavailable.
|
||||
/// </summary>
|
||||
Provenance,
|
||||
|
||||
/// <summary>
|
||||
/// U-VEX: VEX statements conflict or missing applicability data.
|
||||
/// Multiple VEX sources disagree or no VEX coverage exists.
|
||||
/// </summary>
|
||||
VexConflict,
|
||||
|
||||
/// <summary>
|
||||
/// U-FEED: Required knowledge source is missing or stale.
|
||||
/// Advisory feed gap (e.g., no NVD/OSV data for this package).
|
||||
/// </summary>
|
||||
FeedGap,
|
||||
|
||||
/// <summary>
|
||||
/// U-CONFIG: Feature flag or configuration not observable.
|
||||
/// Cannot determine if vulnerable code path is enabled at runtime.
|
||||
/// </summary>
|
||||
ConfigUnknown,
|
||||
|
||||
/// <summary>
|
||||
/// U-ANALYZER: Language or framework not supported by analyzer.
|
||||
/// Static analysis tools do not cover this ecosystem.
|
||||
/// </summary>
|
||||
AnalyzerLimit
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
@@ -24,8 +25,13 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
const string sql = """
|
||||
SELECT set_config('app.current_tenant', @TenantId::text, true);
|
||||
SELECT id, tenant_id, package_id, package_version, band, score,
|
||||
uncertainty_factor, exploit_pressure, first_seen_at,
|
||||
last_evaluated_at, resolution_reason, resolved_at,
|
||||
uncertainty_factor, exploit_pressure,
|
||||
reason_code, remediation_hint,
|
||||
evidence_refs::text as evidence_refs,
|
||||
assumptions::text as assumptions,
|
||||
blast_radius_dependents, blast_radius_net_facing, blast_radius_privilege,
|
||||
containment_seccomp, containment_fs_mode, containment_network_policy,
|
||||
first_seen_at, last_evaluated_at, resolution_reason, resolved_at,
|
||||
created_at, updated_at
|
||||
FROM policy.unknowns
|
||||
WHERE id = @Id;
|
||||
@@ -50,8 +56,13 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
const string sql = """
|
||||
SELECT set_config('app.current_tenant', @TenantId::text, true);
|
||||
SELECT id, tenant_id, package_id, package_version, band, score,
|
||||
uncertainty_factor, exploit_pressure, first_seen_at,
|
||||
last_evaluated_at, resolution_reason, resolved_at,
|
||||
uncertainty_factor, exploit_pressure,
|
||||
reason_code, remediation_hint,
|
||||
evidence_refs::text as evidence_refs,
|
||||
assumptions::text as assumptions,
|
||||
blast_radius_dependents, blast_radius_net_facing, blast_radius_privilege,
|
||||
containment_seccomp, containment_fs_mode, containment_network_policy,
|
||||
first_seen_at, last_evaluated_at, resolution_reason, resolved_at,
|
||||
created_at, updated_at
|
||||
FROM policy.unknowns
|
||||
WHERE package_id = @PackageId AND package_version = @PackageVersion;
|
||||
@@ -76,8 +87,13 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
const string sql = """
|
||||
SELECT set_config('app.current_tenant', @TenantId::text, true);
|
||||
SELECT id, tenant_id, package_id, package_version, band, score,
|
||||
uncertainty_factor, exploit_pressure, first_seen_at,
|
||||
last_evaluated_at, resolution_reason, resolved_at,
|
||||
uncertainty_factor, exploit_pressure,
|
||||
reason_code, remediation_hint,
|
||||
evidence_refs::text as evidence_refs,
|
||||
assumptions::text as assumptions,
|
||||
blast_radius_dependents, blast_radius_net_facing, blast_radius_privilege,
|
||||
containment_seccomp, containment_fs_mode, containment_network_policy,
|
||||
first_seen_at, last_evaluated_at, resolution_reason, resolved_at,
|
||||
created_at, updated_at
|
||||
FROM policy.unknowns
|
||||
WHERE band = @Band
|
||||
@@ -122,18 +138,31 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
SELECT set_config('app.current_tenant', @TenantId::text, true);
|
||||
INSERT INTO policy.unknowns (
|
||||
id, tenant_id, package_id, package_version, band, score,
|
||||
uncertainty_factor, exploit_pressure, first_seen_at,
|
||||
last_evaluated_at, resolution_reason, resolved_at,
|
||||
uncertainty_factor, exploit_pressure,
|
||||
reason_code, remediation_hint,
|
||||
evidence_refs, assumptions,
|
||||
blast_radius_dependents, blast_radius_net_facing, blast_radius_privilege,
|
||||
containment_seccomp, containment_fs_mode, containment_network_policy,
|
||||
first_seen_at, last_evaluated_at, resolution_reason, resolved_at,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
@Id, @TenantId, @PackageId, @PackageVersion, @Band, @Score,
|
||||
@UncertaintyFactor, @ExploitPressure, @FirstSeenAt,
|
||||
@LastEvaluatedAt, @ResolutionReason, @ResolvedAt,
|
||||
@UncertaintyFactor, @ExploitPressure,
|
||||
@ReasonCode, @RemediationHint,
|
||||
@EvidenceRefs::jsonb, @Assumptions::jsonb,
|
||||
@BlastRadiusDependents, @BlastRadiusNetFacing, @BlastRadiusPrivilege,
|
||||
@ContainmentSeccomp, @ContainmentFsMode, @ContainmentNetworkPolicy,
|
||||
@FirstSeenAt, @LastEvaluatedAt, @ResolutionReason, @ResolvedAt,
|
||||
@CreatedAt, @UpdatedAt
|
||||
)
|
||||
RETURNING id, tenant_id, package_id, package_version, band, score,
|
||||
uncertainty_factor, exploit_pressure, first_seen_at,
|
||||
last_evaluated_at, resolution_reason, resolved_at,
|
||||
uncertainty_factor, exploit_pressure,
|
||||
reason_code, remediation_hint,
|
||||
evidence_refs::text as evidence_refs,
|
||||
assumptions::text as assumptions,
|
||||
blast_radius_dependents, blast_radius_net_facing, blast_radius_privilege,
|
||||
containment_seccomp, containment_fs_mode, containment_network_policy,
|
||||
first_seen_at, last_evaluated_at, resolution_reason, resolved_at,
|
||||
created_at, updated_at;
|
||||
""";
|
||||
|
||||
@@ -147,6 +176,16 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
unknown.Score,
|
||||
unknown.UncertaintyFactor,
|
||||
unknown.ExploitPressure,
|
||||
ReasonCode = unknown.ReasonCode.ToString(),
|
||||
unknown.RemediationHint,
|
||||
EvidenceRefs = SerializeEvidenceRefs(unknown.EvidenceRefs),
|
||||
Assumptions = SerializeAssumptions(unknown.Assumptions),
|
||||
BlastRadiusDependents = unknown.BlastRadius?.Dependents,
|
||||
BlastRadiusNetFacing = unknown.BlastRadius?.NetFacing,
|
||||
BlastRadiusPrivilege = unknown.BlastRadius?.Privilege,
|
||||
ContainmentSeccomp = unknown.Containment?.Seccomp,
|
||||
ContainmentFsMode = unknown.Containment?.FileSystem,
|
||||
ContainmentNetworkPolicy = unknown.Containment?.NetworkPolicy,
|
||||
FirstSeenAt = unknown.FirstSeenAt == default ? now : unknown.FirstSeenAt,
|
||||
LastEvaluatedAt = unknown.LastEvaluatedAt == default ? now : unknown.LastEvaluatedAt,
|
||||
unknown.ResolutionReason,
|
||||
@@ -171,6 +210,16 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
score = @Score,
|
||||
uncertainty_factor = @UncertaintyFactor,
|
||||
exploit_pressure = @ExploitPressure,
|
||||
reason_code = @ReasonCode,
|
||||
remediation_hint = @RemediationHint,
|
||||
evidence_refs = @EvidenceRefs::jsonb,
|
||||
assumptions = @Assumptions::jsonb,
|
||||
blast_radius_dependents = COALESCE(@BlastRadiusDependents, blast_radius_dependents),
|
||||
blast_radius_net_facing = COALESCE(@BlastRadiusNetFacing, blast_radius_net_facing),
|
||||
blast_radius_privilege = COALESCE(@BlastRadiusPrivilege, blast_radius_privilege),
|
||||
containment_seccomp = COALESCE(@ContainmentSeccomp, containment_seccomp),
|
||||
containment_fs_mode = COALESCE(@ContainmentFsMode, containment_fs_mode),
|
||||
containment_network_policy = COALESCE(@ContainmentNetworkPolicy, containment_network_policy),
|
||||
last_evaluated_at = @LastEvaluatedAt,
|
||||
resolution_reason = @ResolutionReason,
|
||||
resolved_at = @ResolvedAt,
|
||||
@@ -178,6 +227,7 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
WHERE id = @Id;
|
||||
""";
|
||||
|
||||
var evaluatedAt = DateTimeOffset.UtcNow;
|
||||
var param = new
|
||||
{
|
||||
unknown.TenantId,
|
||||
@@ -186,10 +236,20 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
unknown.Score,
|
||||
unknown.UncertaintyFactor,
|
||||
unknown.ExploitPressure,
|
||||
unknown.LastEvaluatedAt,
|
||||
ReasonCode = unknown.ReasonCode.ToString(),
|
||||
unknown.RemediationHint,
|
||||
EvidenceRefs = SerializeEvidenceRefs(unknown.EvidenceRefs),
|
||||
Assumptions = SerializeAssumptions(unknown.Assumptions),
|
||||
BlastRadiusDependents = unknown.BlastRadius?.Dependents,
|
||||
BlastRadiusNetFacing = unknown.BlastRadius?.NetFacing,
|
||||
BlastRadiusPrivilege = unknown.BlastRadius?.Privilege,
|
||||
ContainmentSeccomp = unknown.Containment?.Seccomp,
|
||||
ContainmentFsMode = unknown.Containment?.FileSystem,
|
||||
ContainmentNetworkPolicy = unknown.Containment?.NetworkPolicy,
|
||||
LastEvaluatedAt = evaluatedAt,
|
||||
unknown.ResolutionReason,
|
||||
unknown.ResolvedAt,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
UpdatedAt = evaluatedAt
|
||||
};
|
||||
|
||||
var affected = await _connection.ExecuteAsync(sql, param);
|
||||
@@ -240,13 +300,21 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
SELECT set_config('app.current_tenant', @TenantId::text, true);
|
||||
INSERT INTO policy.unknowns (
|
||||
id, tenant_id, package_id, package_version, band, score,
|
||||
uncertainty_factor, exploit_pressure, first_seen_at,
|
||||
last_evaluated_at, resolution_reason, resolved_at,
|
||||
uncertainty_factor, exploit_pressure,
|
||||
reason_code, remediation_hint,
|
||||
evidence_refs, assumptions,
|
||||
blast_radius_dependents, blast_radius_net_facing, blast_radius_privilege,
|
||||
containment_seccomp, containment_fs_mode, containment_network_policy,
|
||||
first_seen_at, last_evaluated_at, resolution_reason, resolved_at,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
@Id, @TenantId, @PackageId, @PackageVersion, @Band, @Score,
|
||||
@UncertaintyFactor, @ExploitPressure, @FirstSeenAt,
|
||||
@LastEvaluatedAt, @ResolutionReason, @ResolvedAt,
|
||||
@UncertaintyFactor, @ExploitPressure,
|
||||
@ReasonCode, @RemediationHint,
|
||||
@EvidenceRefs::jsonb, @Assumptions::jsonb,
|
||||
@BlastRadiusDependents, @BlastRadiusNetFacing, @BlastRadiusPrivilege,
|
||||
@ContainmentSeccomp, @ContainmentFsMode, @ContainmentNetworkPolicy,
|
||||
@FirstSeenAt, @LastEvaluatedAt, @ResolutionReason, @ResolvedAt,
|
||||
@CreatedAt, @UpdatedAt
|
||||
)
|
||||
ON CONFLICT (tenant_id, package_id, package_version)
|
||||
@@ -255,6 +323,16 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
score = EXCLUDED.score,
|
||||
uncertainty_factor = EXCLUDED.uncertainty_factor,
|
||||
exploit_pressure = EXCLUDED.exploit_pressure,
|
||||
reason_code = EXCLUDED.reason_code,
|
||||
remediation_hint = EXCLUDED.remediation_hint,
|
||||
evidence_refs = EXCLUDED.evidence_refs,
|
||||
assumptions = EXCLUDED.assumptions,
|
||||
blast_radius_dependents = COALESCE(EXCLUDED.blast_radius_dependents, blast_radius_dependents),
|
||||
blast_radius_net_facing = COALESCE(EXCLUDED.blast_radius_net_facing, blast_radius_net_facing),
|
||||
blast_radius_privilege = COALESCE(EXCLUDED.blast_radius_privilege, blast_radius_privilege),
|
||||
containment_seccomp = COALESCE(EXCLUDED.containment_seccomp, containment_seccomp),
|
||||
containment_fs_mode = COALESCE(EXCLUDED.containment_fs_mode, containment_fs_mode),
|
||||
containment_network_policy = COALESCE(EXCLUDED.containment_network_policy, containment_network_policy),
|
||||
last_evaluated_at = EXCLUDED.last_evaluated_at,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
""";
|
||||
@@ -272,6 +350,16 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
unknown.Score,
|
||||
unknown.UncertaintyFactor,
|
||||
unknown.ExploitPressure,
|
||||
ReasonCode = unknown.ReasonCode.ToString(),
|
||||
unknown.RemediationHint,
|
||||
EvidenceRefs = SerializeEvidenceRefs(unknown.EvidenceRefs),
|
||||
Assumptions = SerializeAssumptions(unknown.Assumptions),
|
||||
BlastRadiusDependents = unknown.BlastRadius?.Dependents,
|
||||
BlastRadiusNetFacing = unknown.BlastRadius?.NetFacing,
|
||||
BlastRadiusPrivilege = unknown.BlastRadius?.Privilege,
|
||||
ContainmentSeccomp = unknown.Containment?.Seccomp,
|
||||
ContainmentFsMode = unknown.Containment?.FileSystem,
|
||||
ContainmentNetworkPolicy = unknown.Containment?.NetworkPolicy,
|
||||
FirstSeenAt = unknown.FirstSeenAt == default ? now : unknown.FirstSeenAt,
|
||||
LastEvaluatedAt = now,
|
||||
unknown.ResolutionReason,
|
||||
@@ -298,6 +386,16 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
decimal score,
|
||||
decimal uncertainty_factor,
|
||||
decimal exploit_pressure,
|
||||
string? reason_code,
|
||||
string? remediation_hint,
|
||||
string? evidence_refs,
|
||||
string? assumptions,
|
||||
int? blast_radius_dependents,
|
||||
bool? blast_radius_net_facing,
|
||||
string? blast_radius_privilege,
|
||||
string? containment_seccomp,
|
||||
string? containment_fs_mode,
|
||||
string? containment_network_policy,
|
||||
DateTimeOffset first_seen_at,
|
||||
DateTimeOffset last_evaluated_at,
|
||||
string? resolution_reason,
|
||||
@@ -315,6 +413,30 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
Score = score,
|
||||
UncertaintyFactor = uncertainty_factor,
|
||||
ExploitPressure = exploit_pressure,
|
||||
ReasonCode = ParseReasonCode(reason_code),
|
||||
RemediationHint = remediation_hint,
|
||||
EvidenceRefs = ParseEvidenceRefs(evidence_refs),
|
||||
Assumptions = ParseAssumptions(assumptions),
|
||||
BlastRadius = blast_radius_dependents.HasValue ||
|
||||
blast_radius_net_facing.HasValue ||
|
||||
!string.IsNullOrEmpty(blast_radius_privilege)
|
||||
? new BlastRadius
|
||||
{
|
||||
Dependents = blast_radius_dependents ?? 0,
|
||||
NetFacing = blast_radius_net_facing ?? false,
|
||||
Privilege = blast_radius_privilege
|
||||
}
|
||||
: null,
|
||||
Containment = !string.IsNullOrEmpty(containment_seccomp) ||
|
||||
!string.IsNullOrEmpty(containment_fs_mode) ||
|
||||
!string.IsNullOrEmpty(containment_network_policy)
|
||||
? new ContainmentSignals
|
||||
{
|
||||
Seccomp = containment_seccomp,
|
||||
FileSystem = containment_fs_mode,
|
||||
NetworkPolicy = containment_network_policy
|
||||
}
|
||||
: null,
|
||||
FirstSeenAt = first_seen_at,
|
||||
LastEvaluatedAt = last_evaluated_at,
|
||||
ResolutionReason = resolution_reason,
|
||||
@@ -326,5 +448,54 @@ public sealed class UnknownsRepository : IUnknownsRepository
|
||||
|
||||
private sealed record SummaryRow(int hot_count, int warm_count, int cold_count, int resolved_count);
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private static IReadOnlyList<EvidenceRef> ParseEvidenceRefs(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return Array.Empty<EvidenceRef>();
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<IReadOnlyList<EvidenceRef>>(json, JsonOptions)
|
||||
?? Array.Empty<EvidenceRef>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Array.Empty<EvidenceRef>();
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ParseAssumptions(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return Array.Empty<string>();
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<IReadOnlyList<string>>(json, JsonOptions)
|
||||
?? Array.Empty<string>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static string SerializeEvidenceRefs(IReadOnlyList<EvidenceRef>? refs) =>
|
||||
JsonSerializer.Serialize(refs ?? Array.Empty<EvidenceRef>(), JsonOptions);
|
||||
|
||||
private static string SerializeAssumptions(IReadOnlyList<string>? assumptions) =>
|
||||
JsonSerializer.Serialize(assumptions ?? Array.Empty<string>(), JsonOptions);
|
||||
|
||||
private static UnknownReasonCode ParseReasonCode(string? value) =>
|
||||
Enum.TryParse<UnknownReasonCode>(value, ignoreCase: true, out var parsed)
|
||||
? parsed
|
||||
: UnknownReasonCode.Reachability;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Policy.Unknowns.Configuration;
|
||||
using StellaOps.Policy.Unknowns.Repositories;
|
||||
using StellaOps.Policy.Unknowns.Services;
|
||||
|
||||
@@ -17,13 +18,18 @@ public static class ServiceCollectionExtensions
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddUnknownsRegistry(
|
||||
this IServiceCollection services,
|
||||
Action<UnknownRankerOptions>? configureOptions = null)
|
||||
Action<UnknownRankerOptions>? configureOptions = null,
|
||||
Action<UnknownBudgetOptions>? configureBudgetOptions = null)
|
||||
{
|
||||
// Configure options
|
||||
if (configureOptions is not null)
|
||||
services.Configure(configureOptions);
|
||||
if (configureBudgetOptions is not null)
|
||||
services.Configure(configureBudgetOptions);
|
||||
|
||||
// Register services
|
||||
services.AddSingleton<IUnknownBudgetService, UnknownBudgetService>();
|
||||
services.AddSingleton<IRemediationHintsRegistry, RemediationHintsRegistry>();
|
||||
services.AddSingleton<IUnknownRanker, UnknownRanker>();
|
||||
services.AddScoped<IUnknownsRepository, UnknownsRepository>();
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Linq;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
namespace StellaOps.Policy.Unknowns.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of remediation hints for each unknown reason code.
|
||||
/// Provides actionable guidance for resolving unknowns.
|
||||
/// </summary>
|
||||
public sealed class RemediationHintsRegistry : IRemediationHintsRegistry
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<UnknownReasonCode, RemediationHint> Hints =
|
||||
new Dictionary<UnknownReasonCode, RemediationHint>
|
||||
{
|
||||
[UnknownReasonCode.Reachability] = new(
|
||||
ShortHint: "Run reachability analysis",
|
||||
DetailedHint: "Execute call-graph analysis to determine if vulnerable code paths are reachable from application entrypoints.",
|
||||
AutomationRef: "stella analyze --reachability"),
|
||||
|
||||
[UnknownReasonCode.Identity] = new(
|
||||
ShortHint: "Add package digest",
|
||||
DetailedHint: "Ensure SBOM includes package checksums (SHA-256) and valid PURL coordinates.",
|
||||
AutomationRef: "stella sbom --include-digests"),
|
||||
|
||||
[UnknownReasonCode.Provenance] = new(
|
||||
ShortHint: "Add provenance attestation",
|
||||
DetailedHint: "Generate SLSA provenance linking binary artifact to source repository and build.",
|
||||
AutomationRef: "stella attest --provenance"),
|
||||
|
||||
[UnknownReasonCode.VexConflict] = new(
|
||||
ShortHint: "Publish authoritative VEX",
|
||||
DetailedHint: "Create or update VEX document with applicability assessment for your deployment context.",
|
||||
AutomationRef: "stella vex create"),
|
||||
|
||||
[UnknownReasonCode.FeedGap] = new(
|
||||
ShortHint: "Add advisory source",
|
||||
DetailedHint: "Configure additional advisory feeds (OSV, vendor-specific) or request coverage from upstream.",
|
||||
AutomationRef: "stella feed add"),
|
||||
|
||||
[UnknownReasonCode.ConfigUnknown] = new(
|
||||
ShortHint: "Document feature flags",
|
||||
DetailedHint: "Export runtime configuration showing which features are enabled/disabled in this deployment.",
|
||||
AutomationRef: "stella config export"),
|
||||
|
||||
[UnknownReasonCode.AnalyzerLimit] = new(
|
||||
ShortHint: "Request analyzer support",
|
||||
DetailedHint: "This language/framework is not yet supported. File an issue or use manual assessment.",
|
||||
AutomationRef: null)
|
||||
};
|
||||
|
||||
public RemediationHint GetHint(UnknownReasonCode code) =>
|
||||
Hints.TryGetValue(code, out var hint) ? hint : RemediationHint.Empty;
|
||||
|
||||
public IEnumerable<(UnknownReasonCode Code, RemediationHint Hint)> GetAllHints() =>
|
||||
Hints.Select(kv => (kv.Key, kv.Value));
|
||||
}
|
||||
|
||||
public sealed record RemediationHint(
|
||||
string ShortHint,
|
||||
string DetailedHint,
|
||||
string? AutomationRef)
|
||||
{
|
||||
public static RemediationHint Empty { get; } = new("No remediation available", string.Empty, null);
|
||||
}
|
||||
|
||||
public interface IRemediationHintsRegistry
|
||||
{
|
||||
RemediationHint GetHint(UnknownReasonCode code);
|
||||
IEnumerable<(UnknownReasonCode Code, RemediationHint Hint)> GetAllHints();
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Unknowns.Configuration;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
|
||||
namespace StellaOps.Policy.Unknowns.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing and checking unknown budgets.
|
||||
/// </summary>
|
||||
public sealed class UnknownBudgetService : IUnknownBudgetService
|
||||
{
|
||||
private static readonly string[] ReasonCodeMetadataKeys =
|
||||
[
|
||||
"unknownReasonCodes",
|
||||
"unknown_reason_codes",
|
||||
"unknown-reason-codes"
|
||||
];
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, UnknownReasonCode> ShortCodeMap =
|
||||
new Dictionary<string, UnknownReasonCode>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["U-RCH"] = UnknownReasonCode.Reachability,
|
||||
["U-ID"] = UnknownReasonCode.Identity,
|
||||
["U-PROV"] = UnknownReasonCode.Provenance,
|
||||
["U-VEX"] = UnknownReasonCode.VexConflict,
|
||||
["U-FEED"] = UnknownReasonCode.FeedGap,
|
||||
["U-CONFIG"] = UnknownReasonCode.ConfigUnknown,
|
||||
["U-ANALYZER"] = UnknownReasonCode.AnalyzerLimit
|
||||
};
|
||||
|
||||
private readonly IOptionsMonitor<UnknownBudgetOptions> _options;
|
||||
private readonly ILogger<UnknownBudgetService> _logger;
|
||||
|
||||
public UnknownBudgetService(
|
||||
IOptionsMonitor<UnknownBudgetOptions> options,
|
||||
ILogger<UnknownBudgetService> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UnknownBudget GetBudgetForEnvironment(string environment)
|
||||
{
|
||||
var normalized = NormalizeEnvironment(environment);
|
||||
var budgets = _options.CurrentValue.Budgets
|
||||
?? new Dictionary<string, UnknownBudget>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (budgets.TryGetValue(normalized, out var budget))
|
||||
{
|
||||
return NormalizeBudget(budget, normalized);
|
||||
}
|
||||
|
||||
if (budgets.TryGetValue("default", out var defaultBudget))
|
||||
{
|
||||
return NormalizeBudget(defaultBudget, normalized);
|
||||
}
|
||||
|
||||
return new UnknownBudget
|
||||
{
|
||||
Environment = normalized,
|
||||
TotalLimit = null,
|
||||
Action = BudgetAction.Warn
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public BudgetCheckResult CheckBudget(
|
||||
string environment,
|
||||
IReadOnlyList<Unknown> unknowns)
|
||||
{
|
||||
var normalized = NormalizeEnvironment(environment);
|
||||
var budget = GetBudgetForEnvironment(normalized);
|
||||
var safeUnknowns = unknowns ?? Array.Empty<Unknown>();
|
||||
|
||||
var byReason = CountByReason(safeUnknowns);
|
||||
var violations = BuildViolations(budget, byReason);
|
||||
var total = safeUnknowns.Count;
|
||||
var totalExceeded = budget.TotalLimit.HasValue && total > budget.TotalLimit.Value;
|
||||
var isWithinBudget = violations.Count == 0 && !totalExceeded;
|
||||
|
||||
var action = isWithinBudget ? BudgetAction.Warn : budget.Action;
|
||||
if (!isWithinBudget && !_options.CurrentValue.EnforceBudgets)
|
||||
{
|
||||
action = BudgetAction.Warn;
|
||||
}
|
||||
|
||||
var message = isWithinBudget
|
||||
? null
|
||||
: budget.ExceededMessage ?? $"Unknown budget exceeded: {total} unknowns in {normalized}";
|
||||
|
||||
return new BudgetCheckResult
|
||||
{
|
||||
IsWithinBudget = isWithinBudget,
|
||||
RecommendedAction = action,
|
||||
TotalUnknowns = total,
|
||||
TotalLimit = budget.TotalLimit,
|
||||
Violations = violations,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public BudgetCheckResult CheckBudgetWithEscalation(
|
||||
string environment,
|
||||
IReadOnlyList<Unknown> unknowns,
|
||||
IReadOnlyList<ExceptionObject>? exceptions = null)
|
||||
{
|
||||
var normalized = NormalizeEnvironment(environment);
|
||||
var baseResult = CheckBudget(normalized, unknowns);
|
||||
if (baseResult.IsWithinBudget || exceptions is null || exceptions.Count == 0)
|
||||
{
|
||||
return baseResult;
|
||||
}
|
||||
|
||||
var coveredReasons = CollectCoveredReasons(exceptions, normalized);
|
||||
if (coveredReasons.Count == 0)
|
||||
{
|
||||
LogViolation(normalized, baseResult);
|
||||
return baseResult;
|
||||
}
|
||||
|
||||
var byReason = CountByReason(unknowns ?? Array.Empty<Unknown>());
|
||||
var totalExceeded = baseResult.TotalLimit.HasValue && baseResult.TotalUnknowns > baseResult.TotalLimit.Value;
|
||||
var violationsCovered = baseResult.Violations.Keys.All(coveredReasons.Contains);
|
||||
var totalCovered = !totalExceeded || byReason.Keys.All(coveredReasons.Contains);
|
||||
|
||||
if (violationsCovered && totalCovered)
|
||||
{
|
||||
return baseResult with
|
||||
{
|
||||
IsWithinBudget = true,
|
||||
RecommendedAction = BudgetAction.Warn,
|
||||
Message = "Budget exceeded but covered by approved exceptions"
|
||||
};
|
||||
}
|
||||
|
||||
LogViolation(normalized, baseResult);
|
||||
return baseResult;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public BudgetStatusSummary GetBudgetStatus(
|
||||
string environment,
|
||||
IReadOnlyList<Unknown> unknowns)
|
||||
{
|
||||
var normalized = NormalizeEnvironment(environment);
|
||||
var budget = GetBudgetForEnvironment(normalized);
|
||||
var safeUnknowns = unknowns ?? Array.Empty<Unknown>();
|
||||
var byReason = CountByReason(safeUnknowns);
|
||||
var result = CheckBudget(normalized, safeUnknowns);
|
||||
|
||||
var percentage = budget.TotalLimit.HasValue && budget.TotalLimit.Value > 0
|
||||
? (decimal)safeUnknowns.Count / budget.TotalLimit.Value * 100m
|
||||
: 0m;
|
||||
|
||||
return new BudgetStatusSummary
|
||||
{
|
||||
Environment = normalized,
|
||||
TotalUnknowns = safeUnknowns.Count,
|
||||
TotalLimit = budget.TotalLimit,
|
||||
PercentageUsed = percentage,
|
||||
IsExceeded = !result.IsWithinBudget,
|
||||
ViolationCount = result.Violations.Count,
|
||||
ByReasonCode = byReason
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShouldBlock(BudgetCheckResult result) =>
|
||||
!result.IsWithinBudget && result.RecommendedAction == BudgetAction.Block;
|
||||
|
||||
private static string NormalizeEnvironment(string environment) =>
|
||||
string.IsNullOrWhiteSpace(environment) ? "default" : environment.Trim();
|
||||
|
||||
private static UnknownBudget NormalizeBudget(UnknownBudget budget, string environment)
|
||||
{
|
||||
var reasonLimits = budget.ReasonLimits ?? new Dictionary<UnknownReasonCode, int>();
|
||||
return budget with
|
||||
{
|
||||
Environment = environment,
|
||||
ReasonLimits = reasonLimits
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<UnknownReasonCode, int> CountByReason(IReadOnlyList<Unknown> unknowns) =>
|
||||
unknowns
|
||||
.GroupBy(u => u.ReasonCode)
|
||||
.OrderBy(g => g.Key)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
private static IReadOnlyDictionary<UnknownReasonCode, BudgetViolation> BuildViolations(
|
||||
UnknownBudget budget,
|
||||
IReadOnlyDictionary<UnknownReasonCode, int> byReason)
|
||||
{
|
||||
var violations = new Dictionary<UnknownReasonCode, BudgetViolation>();
|
||||
|
||||
foreach (var entry in budget.ReasonLimits.OrderBy(r => r.Key))
|
||||
{
|
||||
if (byReason.TryGetValue(entry.Key, out var count) && count > entry.Value)
|
||||
{
|
||||
violations[entry.Key] = new BudgetViolation(entry.Key, count, entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
private static HashSet<UnknownReasonCode> CollectCoveredReasons(
|
||||
IReadOnlyList<ExceptionObject> exceptions,
|
||||
string environment)
|
||||
{
|
||||
var covered = new HashSet<UnknownReasonCode>();
|
||||
|
||||
foreach (var exception in exceptions)
|
||||
{
|
||||
if (exception.Type != ExceptionType.Unknown)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (exception.Status is not (ExceptionStatus.Approved or ExceptionStatus.Active))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (exception.Scope.Environments.Length > 0
|
||||
&& !exception.Scope.Environments.Any(env => env.Equals(environment, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var reasons = ParseCoveredReasonCodes(exception);
|
||||
if (reasons.Count == 0)
|
||||
{
|
||||
foreach (var code in Enum.GetValues<UnknownReasonCode>())
|
||||
{
|
||||
covered.Add(code);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var code in reasons)
|
||||
{
|
||||
covered.Add(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return covered;
|
||||
}
|
||||
|
||||
private static HashSet<UnknownReasonCode> ParseCoveredReasonCodes(ExceptionObject exception)
|
||||
{
|
||||
foreach (var key in ReasonCodeMetadataKeys)
|
||||
{
|
||||
if (exception.Metadata.TryGetValue(key, out var value))
|
||||
{
|
||||
return ParseReasonCodes(value);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static HashSet<UnknownReasonCode> ParseReasonCodes(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var tokens = raw.Split([',', ';', '|'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var codes = new HashSet<UnknownReasonCode>();
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (ShortCodeMap.TryGetValue(token, out var shortCode))
|
||||
{
|
||||
codes.Add(shortCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
var cleaned = token.Replace("U-", "", StringComparison.OrdinalIgnoreCase).Trim();
|
||||
if (Enum.TryParse(cleaned, ignoreCase: true, out UnknownReasonCode parsed))
|
||||
{
|
||||
codes.Add(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
private void LogViolation(string environment, BudgetCheckResult result)
|
||||
{
|
||||
if (result.IsWithinBudget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Unknown budget exceeded for environment {Environment}: {Total}/{Limit}",
|
||||
environment,
|
||||
result.TotalUnknowns,
|
||||
result.TotalLimit?.ToString(CultureInfo.InvariantCulture) ?? "none");
|
||||
}
|
||||
}
|
||||
|
||||
public interface IUnknownBudgetService
|
||||
{
|
||||
UnknownBudget GetBudgetForEnvironment(string environment);
|
||||
BudgetCheckResult CheckBudget(string environment, IReadOnlyList<Unknown> unknowns);
|
||||
BudgetCheckResult CheckBudgetWithEscalation(
|
||||
string environment,
|
||||
IReadOnlyList<Unknown> unknowns,
|
||||
IReadOnlyList<ExceptionObject>? exceptions = null);
|
||||
BudgetStatusSummary GetBudgetStatus(string environment, IReadOnlyList<Unknown> unknowns);
|
||||
bool ShouldBlock(BudgetCheckResult result);
|
||||
}
|
||||
@@ -13,6 +13,17 @@ namespace StellaOps.Policy.Unknowns.Services;
|
||||
/// <param name="IsInKev">Whether the CVE is in the CISA KEV list.</param>
|
||||
/// <param name="EpssScore">EPSS score (0.0 - 1.0).</param>
|
||||
/// <param name="CvssScore">CVSS base score (0.0 - 10.0).</param>
|
||||
/// <param name="FirstSeenAt">When the unknown was first observed.</param>
|
||||
/// <param name="LastEvaluatedAt">When the unknown was last re-ranked.</param>
|
||||
/// <param name="AsOfDateTime">Reference time for decay calculations.</param>
|
||||
/// <param name="BlastRadius">Dependency impact signals for containment reduction.</param>
|
||||
/// <param name="Containment">Runtime containment posture signals.</param>
|
||||
/// <param name="HasPackageDigest">Whether a package digest is available.</param>
|
||||
/// <param name="HasProvenanceAttestation">Whether provenance attestation exists.</param>
|
||||
/// <param name="HasVexConflicts">Whether VEX statements conflict.</param>
|
||||
/// <param name="HasFeedCoverage">Whether advisory feeds cover this package.</param>
|
||||
/// <param name="HasConfigVisibility">Whether configuration visibility is available.</param>
|
||||
/// <param name="IsAnalyzerSupported">Whether analyzer supports this ecosystem.</param>
|
||||
public sealed record UnknownRankInput(
|
||||
bool HasVexStatement,
|
||||
bool HasReachabilityData,
|
||||
@@ -20,7 +31,18 @@ public sealed record UnknownRankInput(
|
||||
bool IsStaleAdvisory,
|
||||
bool IsInKev,
|
||||
decimal EpssScore,
|
||||
decimal CvssScore);
|
||||
decimal CvssScore,
|
||||
DateTimeOffset? FirstSeenAt,
|
||||
DateTimeOffset? LastEvaluatedAt,
|
||||
DateTimeOffset AsOfDateTime,
|
||||
BlastRadius? BlastRadius,
|
||||
ContainmentSignals? Containment,
|
||||
bool HasPackageDigest,
|
||||
bool HasProvenanceAttestation,
|
||||
bool HasVexConflicts,
|
||||
bool HasFeedCoverage,
|
||||
bool HasConfigVisibility,
|
||||
bool IsAnalyzerSupported);
|
||||
|
||||
/// <summary>
|
||||
/// Result of unknown ranking calculation.
|
||||
@@ -29,11 +51,19 @@ public sealed record UnknownRankInput(
|
||||
/// <param name="UncertaintyFactor">Uncertainty component (0.0000 - 1.0000).</param>
|
||||
/// <param name="ExploitPressure">Exploit pressure component (0.0000 - 1.0000).</param>
|
||||
/// <param name="Band">Assigned band based on score thresholds.</param>
|
||||
/// <param name="DecayFactor">Applied time-based decay multiplier.</param>
|
||||
/// <param name="ContainmentReduction">Applied containment reduction factor.</param>
|
||||
/// <param name="ReasonCode">Primary reason code for the unknown classification.</param>
|
||||
/// <param name="RemediationHint">Short remediation hint for the reason code.</param>
|
||||
public sealed record UnknownRankResult(
|
||||
decimal Score,
|
||||
decimal UncertaintyFactor,
|
||||
decimal ExploitPressure,
|
||||
UnknownBand Band);
|
||||
UnknownBand Band,
|
||||
decimal DecayFactor = 1.0m,
|
||||
decimal ContainmentReduction = 0m,
|
||||
UnknownReasonCode ReasonCode = UnknownReasonCode.Reachability,
|
||||
string? RemediationHint = null);
|
||||
|
||||
/// <summary>
|
||||
/// Service for computing deterministic unknown rankings.
|
||||
@@ -74,9 +104,13 @@ public interface IUnknownRanker
|
||||
public sealed class UnknownRanker : IUnknownRanker
|
||||
{
|
||||
private readonly UnknownRankerOptions _options;
|
||||
private readonly IRemediationHintsRegistry _hintsRegistry;
|
||||
|
||||
public UnknownRanker(IOptions<UnknownRankerOptions> options)
|
||||
=> _options = options.Value;
|
||||
public UnknownRanker(IOptions<UnknownRankerOptions> options, IRemediationHintsRegistry? hintsRegistry = null)
|
||||
{
|
||||
_options = options.Value;
|
||||
_hintsRegistry = hintsRegistry ?? new RemediationHintsRegistry();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor for simple usage without DI.
|
||||
@@ -88,10 +122,29 @@ public sealed class UnknownRanker : IUnknownRanker
|
||||
{
|
||||
var uncertainty = ComputeUncertainty(input);
|
||||
var pressure = ComputeExploitPressure(input);
|
||||
var score = Math.Round((uncertainty * 50m) + (pressure * 50m), 2);
|
||||
var band = AssignBand(score);
|
||||
var rawScore = Math.Round((uncertainty * 50m) + (pressure * 50m), 2);
|
||||
|
||||
return new UnknownRankResult(score, uncertainty, pressure, band);
|
||||
var decayFactor = _options.EnableDecay ? ComputeDecayFactor(input) : 1.0m;
|
||||
var decayedScore = Math.Round(rawScore * decayFactor, 2);
|
||||
|
||||
var containmentReduction = _options.EnableContainmentReduction
|
||||
? ComputeContainmentReduction(input)
|
||||
: 0m;
|
||||
var finalScore = Math.Round(Math.Max(0m, decayedScore * (1m - containmentReduction)), 2);
|
||||
|
||||
var band = AssignBand(finalScore);
|
||||
var reasonCode = DetermineReasonCode(input);
|
||||
var hint = _hintsRegistry.GetHint(reasonCode);
|
||||
|
||||
return new UnknownRankResult(
|
||||
finalScore,
|
||||
uncertainty,
|
||||
pressure,
|
||||
band,
|
||||
decayFactor,
|
||||
containmentReduction,
|
||||
reasonCode,
|
||||
hint.ShortHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -144,16 +197,113 @@ public sealed class UnknownRanker : IUnknownRanker
|
||||
return Math.Min(pressure, 1.0m);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes time-based decay factor for stale unknowns.
|
||||
/// </summary>
|
||||
private decimal ComputeDecayFactor(UnknownRankInput input)
|
||||
{
|
||||
if (input.LastEvaluatedAt is null)
|
||||
return 1.0m;
|
||||
|
||||
if (_options.DecayBuckets is null || _options.DecayBuckets.Count == 0)
|
||||
return 1.0m;
|
||||
|
||||
var ageDays = (int)Math.Max(0, (input.AsOfDateTime - input.LastEvaluatedAt.Value).TotalDays);
|
||||
DecayBucket? selected = null;
|
||||
|
||||
foreach (var bucket in _options.DecayBuckets)
|
||||
{
|
||||
if (bucket.MaxAgeDays >= ageDays &&
|
||||
(selected is null || bucket.MaxAgeDays < selected.MaxAgeDays))
|
||||
{
|
||||
selected = bucket;
|
||||
}
|
||||
}
|
||||
|
||||
if (selected is null)
|
||||
return 1.0m;
|
||||
|
||||
var clamped = Math.Clamp(selected.MultiplierBps, 0, 10000);
|
||||
return clamped / 10000m;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes containment-based reduction factor.
|
||||
/// </summary>
|
||||
private decimal ComputeContainmentReduction(UnknownRankInput input)
|
||||
{
|
||||
decimal reduction = 0m;
|
||||
|
||||
if (input.BlastRadius is { } blast)
|
||||
{
|
||||
if (blast.Dependents == 0)
|
||||
reduction += _options.IsolatedReduction;
|
||||
|
||||
if (!blast.NetFacing)
|
||||
reduction += _options.NotNetFacingReduction;
|
||||
|
||||
if (blast.Privilege is "user" or "none")
|
||||
reduction += _options.NonRootReduction;
|
||||
}
|
||||
|
||||
if (input.Containment is { } containment)
|
||||
{
|
||||
if (containment.Seccomp == "enforced")
|
||||
reduction += _options.SeccompEnforcedReduction;
|
||||
|
||||
if (containment.FileSystem == "ro")
|
||||
reduction += _options.FsReadOnlyReduction;
|
||||
|
||||
if (containment.NetworkPolicy == "isolated")
|
||||
reduction += _options.NetworkIsolatedReduction;
|
||||
}
|
||||
|
||||
return Math.Min(reduction, _options.MaxContainmentReduction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns band based on score thresholds.
|
||||
/// </summary>
|
||||
private UnknownBand AssignBand(decimal score) => score switch
|
||||
private UnknownBand AssignBand(decimal score)
|
||||
{
|
||||
>= 75m => UnknownBand.Hot, // Hot threshold (configurable)
|
||||
>= 50m => UnknownBand.Warm, // Warm threshold
|
||||
>= 25m => UnknownBand.Cold, // Cold threshold
|
||||
_ => UnknownBand.Resolved // Below cold = resolved
|
||||
};
|
||||
if (score >= _options.HotThreshold)
|
||||
return UnknownBand.Hot;
|
||||
if (score >= _options.WarmThreshold)
|
||||
return UnknownBand.Warm;
|
||||
if (score >= _options.ColdThreshold)
|
||||
return UnknownBand.Cold;
|
||||
return UnknownBand.Resolved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the primary reason code for unknown classification.
|
||||
/// Returns the most actionable/resolvable reason.
|
||||
/// </summary>
|
||||
private static UnknownReasonCode DetermineReasonCode(UnknownRankInput input)
|
||||
{
|
||||
if (!input.IsAnalyzerSupported)
|
||||
return UnknownReasonCode.AnalyzerLimit;
|
||||
|
||||
if (!input.HasReachabilityData)
|
||||
return UnknownReasonCode.Reachability;
|
||||
|
||||
if (!input.HasPackageDigest)
|
||||
return UnknownReasonCode.Identity;
|
||||
|
||||
if (!input.HasProvenanceAttestation)
|
||||
return UnknownReasonCode.Provenance;
|
||||
|
||||
if (input.HasVexConflicts || !input.HasVexStatement)
|
||||
return UnknownReasonCode.VexConflict;
|
||||
|
||||
if (!input.HasFeedCoverage)
|
||||
return UnknownReasonCode.FeedGap;
|
||||
|
||||
if (!input.HasConfigVisibility)
|
||||
return UnknownReasonCode.ConfigUnknown;
|
||||
|
||||
return UnknownReasonCode.Reachability;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -169,4 +319,50 @@ public sealed class UnknownRankerOptions
|
||||
|
||||
/// <summary>Score threshold for COLD band (default: 25).</summary>
|
||||
public decimal ColdThreshold { get; set; } = 25m;
|
||||
|
||||
/// <summary>Enable time-based score decay.</summary>
|
||||
public bool EnableDecay { get; set; } = true;
|
||||
|
||||
/// <summary>Decay buckets ordered by maximum age in days.</summary>
|
||||
public IReadOnlyList<DecayBucket> DecayBuckets { get; set; } = DefaultDecayBuckets;
|
||||
|
||||
/// <summary>Default decay buckets using basis points.</summary>
|
||||
public static IReadOnlyList<DecayBucket> DefaultDecayBuckets { get; } =
|
||||
[
|
||||
new DecayBucket(7, 10000),
|
||||
new DecayBucket(30, 9000),
|
||||
new DecayBucket(90, 7500),
|
||||
new DecayBucket(180, 6000),
|
||||
new DecayBucket(365, 4000),
|
||||
new DecayBucket(int.MaxValue, 2000)
|
||||
];
|
||||
|
||||
/// <summary>Enable containment-based reduction.</summary>
|
||||
public bool EnableContainmentReduction { get; set; } = true;
|
||||
|
||||
/// <summary>Reduction for isolated package (dependents=0).</summary>
|
||||
public decimal IsolatedReduction { get; set; } = 0.15m;
|
||||
|
||||
/// <summary>Reduction for not network-facing packages.</summary>
|
||||
public decimal NotNetFacingReduction { get; set; } = 0.05m;
|
||||
|
||||
/// <summary>Reduction for non-root privilege.</summary>
|
||||
public decimal NonRootReduction { get; set; } = 0.05m;
|
||||
|
||||
/// <summary>Reduction for enforced Seccomp.</summary>
|
||||
public decimal SeccompEnforcedReduction { get; set; } = 0.10m;
|
||||
|
||||
/// <summary>Reduction for read-only filesystem.</summary>
|
||||
public decimal FsReadOnlyReduction { get; set; } = 0.10m;
|
||||
|
||||
/// <summary>Reduction for isolated network policy.</summary>
|
||||
public decimal NetworkIsolatedReduction { get; set; } = 0.05m;
|
||||
|
||||
/// <summary>Maximum reduction allowed from containment signals.</summary>
|
||||
public decimal MaxContainmentReduction { get; set; } = 0.40m;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a decay bucket using basis points.
|
||||
/// </summary>
|
||||
public sealed record DecayBucket(int MaxAgeDays, int MultiplierBps);
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Policy.Exceptions\StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Policy\StellaOps.Policy.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Policy.Unknowns;
|
||||
|
||||
/// <summary>
|
||||
/// Unknowns budget configuration for policy evaluation.
|
||||
/// </summary>
|
||||
public sealed record UnknownsBudgetConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum allowed critical severity unknowns.
|
||||
/// </summary>
|
||||
public int MaxCriticalUnknowns { get; init; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed high severity unknowns.
|
||||
/// </summary>
|
||||
public int MaxHighUnknowns { get; init; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed medium severity unknowns.
|
||||
/// </summary>
|
||||
public int MaxMediumUnknowns { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed low severity unknowns.
|
||||
/// </summary>
|
||||
public int MaxLowUnknowns { get; init; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum total unknowns across all severities.
|
||||
/// </summary>
|
||||
public int? MaxTotalUnknowns { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when budget is exceeded.
|
||||
/// </summary>
|
||||
public UnknownsBudgetAction Action { get; init; } = UnknownsBudgetAction.Block;
|
||||
|
||||
/// <summary>
|
||||
/// Environment-specific overrides.
|
||||
/// </summary>
|
||||
public Dictionary<string, UnknownsBudgetConfig>? EnvironmentOverrides { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when unknowns budget is exceeded.
|
||||
/// </summary>
|
||||
public enum UnknownsBudgetAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Block deployment/approval.
|
||||
/// </summary>
|
||||
Block,
|
||||
|
||||
/// <summary>
|
||||
/// Warn but allow deployment.
|
||||
/// </summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>
|
||||
/// Log only, no enforcement.
|
||||
/// </summary>
|
||||
Log
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts of unknowns by severity.
|
||||
/// </summary>
|
||||
public sealed record UnknownsCounts
|
||||
{
|
||||
public int Critical { get; init; }
|
||||
public int High { get; init; }
|
||||
public int Medium { get; init; }
|
||||
public int Low { get; init; }
|
||||
public int Total => Critical + High + Medium + Low;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of unknowns budget enforcement.
|
||||
/// </summary>
|
||||
public sealed record UnknownsBudgetResult
|
||||
{
|
||||
public required bool WithinBudget { get; init; }
|
||||
public required UnknownsCounts Counts { get; init; }
|
||||
public required UnknownsBudgetConfig Budget { get; init; }
|
||||
public required UnknownsBudgetAction Action { get; init; }
|
||||
public IReadOnlyList<string>? Violations { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enforces unknowns budget for policy decisions.
|
||||
/// </summary>
|
||||
public sealed class UnknownsBudgetEnforcer
|
||||
{
|
||||
private readonly ILogger<UnknownsBudgetEnforcer> _logger;
|
||||
|
||||
public UnknownsBudgetEnforcer(ILogger<UnknownsBudgetEnforcer> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate unknowns counts against budget.
|
||||
/// </summary>
|
||||
public UnknownsBudgetResult Evaluate(
|
||||
UnknownsCounts counts,
|
||||
UnknownsBudgetConfig budget,
|
||||
string? environment = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(counts);
|
||||
ArgumentNullException.ThrowIfNull(budget);
|
||||
|
||||
var effectiveBudget = GetEffectiveBudget(budget, environment);
|
||||
var violations = new List<string>();
|
||||
|
||||
if (counts.Critical > effectiveBudget.MaxCriticalUnknowns)
|
||||
{
|
||||
violations.Add($"Critical unknowns ({counts.Critical}) exceeds budget ({effectiveBudget.MaxCriticalUnknowns})");
|
||||
}
|
||||
|
||||
if (counts.High > effectiveBudget.MaxHighUnknowns)
|
||||
{
|
||||
violations.Add($"High unknowns ({counts.High}) exceeds budget ({effectiveBudget.MaxHighUnknowns})");
|
||||
}
|
||||
|
||||
if (counts.Medium > effectiveBudget.MaxMediumUnknowns)
|
||||
{
|
||||
violations.Add($"Medium unknowns ({counts.Medium}) exceeds budget ({effectiveBudget.MaxMediumUnknowns})");
|
||||
}
|
||||
|
||||
if (counts.Low > effectiveBudget.MaxLowUnknowns)
|
||||
{
|
||||
violations.Add($"Low unknowns ({counts.Low}) exceeds budget ({effectiveBudget.MaxLowUnknowns})");
|
||||
}
|
||||
|
||||
if (effectiveBudget.MaxTotalUnknowns.HasValue &&
|
||||
counts.Total > effectiveBudget.MaxTotalUnknowns.Value)
|
||||
{
|
||||
violations.Add($"Total unknowns ({counts.Total}) exceeds budget ({effectiveBudget.MaxTotalUnknowns.Value})");
|
||||
}
|
||||
|
||||
var withinBudget = violations.Count == 0;
|
||||
|
||||
if (!withinBudget)
|
||||
{
|
||||
LogViolations(violations, effectiveBudget.Action, environment);
|
||||
}
|
||||
|
||||
return new UnknownsBudgetResult
|
||||
{
|
||||
WithinBudget = withinBudget,
|
||||
Counts = counts,
|
||||
Budget = effectiveBudget,
|
||||
Action = effectiveBudget.Action,
|
||||
Violations = violations
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if deployment should be blocked based on budget result.
|
||||
/// </summary>
|
||||
public bool ShouldBlock(UnknownsBudgetResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
return !result.WithinBudget && result.Action == UnknownsBudgetAction.Block;
|
||||
}
|
||||
|
||||
private static UnknownsBudgetConfig GetEffectiveBudget(
|
||||
UnknownsBudgetConfig budget,
|
||||
string? environment)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(environment) ||
|
||||
budget.EnvironmentOverrides is null ||
|
||||
!budget.EnvironmentOverrides.TryGetValue(environment, out var override_))
|
||||
{
|
||||
return budget;
|
||||
}
|
||||
|
||||
return override_;
|
||||
}
|
||||
|
||||
private void LogViolations(
|
||||
List<string> violations,
|
||||
UnknownsBudgetAction action,
|
||||
string? environment)
|
||||
{
|
||||
var envStr = string.IsNullOrWhiteSpace(environment) ? "" : $" (env: {environment})";
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case UnknownsBudgetAction.Block:
|
||||
_logger.LogError(
|
||||
"Unknowns budget exceeded{Env}. Blocking deployment. Violations: {Violations}",
|
||||
envStr,
|
||||
string.Join("; ", violations));
|
||||
break;
|
||||
|
||||
case UnknownsBudgetAction.Warn:
|
||||
_logger.LogWarning(
|
||||
"Unknowns budget exceeded{Env}. Allowing deployment with warning. Violations: {Violations}",
|
||||
envStr,
|
||||
string.Join("; ", violations));
|
||||
break;
|
||||
|
||||
case UnknownsBudgetAction.Log:
|
||||
_logger.LogInformation(
|
||||
"Unknowns budget exceeded{Env}. Logging only. Violations: {Violations}",
|
||||
envStr,
|
||||
string.Join("; ", violations));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user