feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Enforces budget constraints on release operations.
|
||||
/// </summary>
|
||||
public sealed class BudgetConstraintEnforcer : IBudgetConstraintEnforcer
|
||||
{
|
||||
private readonly IBudgetLedger _ledger;
|
||||
private readonly IGateSelector _gateSelector;
|
||||
private readonly ILogger<BudgetConstraintEnforcer> _logger;
|
||||
|
||||
public BudgetConstraintEnforcer(
|
||||
IBudgetLedger ledger,
|
||||
IGateSelector gateSelector,
|
||||
ILogger<BudgetConstraintEnforcer>? logger = null)
|
||||
{
|
||||
_ledger = ledger ?? throw new ArgumentNullException(nameof(ledger));
|
||||
_gateSelector = gateSelector ?? throw new ArgumentNullException(nameof(gateSelector));
|
||||
_logger = logger ?? NullLogger<BudgetConstraintEnforcer>.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a release can proceed given current budget.
|
||||
/// </summary>
|
||||
public async Task<BudgetCheckResult> CheckReleaseAsync(
|
||||
ReleaseCheckInput input,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var budget = await _ledger.GetBudgetAsync(input.ServiceId, ct: ct).ConfigureAwait(false);
|
||||
var gateResult = await _gateSelector.SelectGateAsync(input.ToGateInput(), ct).ConfigureAwait(false);
|
||||
|
||||
var result = new BudgetCheckResult
|
||||
{
|
||||
CanProceed = !gateResult.IsBlocked,
|
||||
RequiredGate = gateResult.Gate,
|
||||
RiskPoints = gateResult.RiskScore,
|
||||
BudgetBefore = budget,
|
||||
BudgetAfter = budget with { Consumed = budget.Consumed + gateResult.RiskScore },
|
||||
BlockReason = gateResult.BlockReason,
|
||||
Requirements = gateResult.Requirements,
|
||||
Recommendations = gateResult.Recommendations
|
||||
};
|
||||
|
||||
// Log the check
|
||||
_logger.LogInformation(
|
||||
"Release check for {ServiceId}: CanProceed={CanProceed}, Gate={Gate}, RP={RP}",
|
||||
input.ServiceId, result.CanProceed, result.RequiredGate, result.RiskPoints);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a release and consumes budget.
|
||||
/// </summary>
|
||||
public async Task<ReleaseRecordResult> RecordReleaseAsync(
|
||||
ReleaseRecordInput input,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// First check if release can proceed
|
||||
var checkResult = await CheckReleaseAsync(input.ToCheckInput(), ct).ConfigureAwait(false);
|
||||
|
||||
if (!checkResult.CanProceed)
|
||||
{
|
||||
return new ReleaseRecordResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = checkResult.BlockReason ?? "Release blocked by budget constraints"
|
||||
};
|
||||
}
|
||||
|
||||
// Consume budget
|
||||
var consumeResult = await _ledger.ConsumeAsync(
|
||||
input.ServiceId,
|
||||
checkResult.RiskPoints,
|
||||
input.ReleaseId,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
if (!consumeResult.IsSuccess)
|
||||
{
|
||||
return new ReleaseRecordResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = consumeResult.Error
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Recorded release {ReleaseId} for {ServiceId}. Budget: {Remaining}/{Allocated} RP remaining",
|
||||
input.ReleaseId, input.ServiceId,
|
||||
consumeResult.Budget.Remaining, consumeResult.Budget.Allocated);
|
||||
|
||||
return new ReleaseRecordResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
ReleaseId = input.ReleaseId,
|
||||
ConsumedRiskPoints = checkResult.RiskPoints,
|
||||
Budget = consumeResult.Budget,
|
||||
Gate = checkResult.RequiredGate
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles break-glass exception for urgent releases.
|
||||
/// </summary>
|
||||
public async Task<ExceptionResult> RecordExceptionAsync(
|
||||
ExceptionInput input,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Record the exception
|
||||
var baseRiskPoints = await CalculateBaseRiskPointsAsync(input, ct).ConfigureAwait(false);
|
||||
|
||||
// Apply 50% penalty for exception
|
||||
var penaltyRiskPoints = (int)(baseRiskPoints * 1.5);
|
||||
|
||||
var consumeResult = await _ledger.ConsumeAsync(
|
||||
input.ServiceId,
|
||||
penaltyRiskPoints,
|
||||
input.ReleaseId,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
_logger.LogWarning(
|
||||
"Break-glass exception for {ServiceId}: {ReleaseId}. Penalty: {Penalty} RP. Reason: {Reason}",
|
||||
input.ServiceId, input.ReleaseId, penaltyRiskPoints - baseRiskPoints, input.Reason);
|
||||
|
||||
return new ExceptionResult
|
||||
{
|
||||
IsSuccess = consumeResult.IsSuccess,
|
||||
ReleaseId = input.ReleaseId,
|
||||
BaseRiskPoints = baseRiskPoints,
|
||||
PenaltyRiskPoints = penaltyRiskPoints - baseRiskPoints,
|
||||
TotalRiskPoints = penaltyRiskPoints,
|
||||
Budget = consumeResult.Budget,
|
||||
FollowUpRequired = true,
|
||||
FollowUpDeadline = DateTimeOffset.UtcNow.AddDays(5)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<int> CalculateBaseRiskPointsAsync(ExceptionInput input, CancellationToken ct)
|
||||
{
|
||||
var gateResult = await _gateSelector.SelectGateAsync(new GateSelectionInput
|
||||
{
|
||||
ServiceId = input.ServiceId,
|
||||
Tier = input.Tier,
|
||||
DiffCategory = input.DiffCategory,
|
||||
Context = input.Context,
|
||||
Mitigations = input.Mitigations,
|
||||
IsEmergencyFix = true
|
||||
}, ct).ConfigureAwait(false);
|
||||
|
||||
return gateResult.RiskScore;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for release check.
|
||||
/// </summary>
|
||||
public sealed record ReleaseCheckInput
|
||||
{
|
||||
public required string ServiceId { get; init; }
|
||||
public required ServiceTier Tier { get; init; }
|
||||
public required DiffCategory DiffCategory { get; init; }
|
||||
public required OperationalContext Context { get; init; }
|
||||
public required MitigationFactors Mitigations { get; init; }
|
||||
|
||||
public GateSelectionInput ToGateInput() => new()
|
||||
{
|
||||
ServiceId = ServiceId,
|
||||
Tier = Tier,
|
||||
DiffCategory = DiffCategory,
|
||||
Context = Context,
|
||||
Mitigations = Mitigations
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of budget check.
|
||||
/// </summary>
|
||||
public sealed record BudgetCheckResult
|
||||
{
|
||||
public required bool CanProceed { get; init; }
|
||||
public required GateLevel RequiredGate { get; init; }
|
||||
public required int RiskPoints { get; init; }
|
||||
public required RiskBudget BudgetBefore { get; init; }
|
||||
public required RiskBudget BudgetAfter { get; init; }
|
||||
public string? BlockReason { get; init; }
|
||||
public IReadOnlyList<string> Requirements { get; init; } = [];
|
||||
public IReadOnlyList<string> Recommendations { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for release recording.
|
||||
/// </summary>
|
||||
public sealed record ReleaseRecordInput
|
||||
{
|
||||
public required string ReleaseId { get; init; }
|
||||
public required string ServiceId { get; init; }
|
||||
public required ServiceTier Tier { get; init; }
|
||||
public required DiffCategory DiffCategory { get; init; }
|
||||
public required OperationalContext Context { get; init; }
|
||||
public required MitigationFactors Mitigations { get; init; }
|
||||
|
||||
public ReleaseCheckInput ToCheckInput() => new()
|
||||
{
|
||||
ServiceId = ServiceId,
|
||||
Tier = Tier,
|
||||
DiffCategory = DiffCategory,
|
||||
Context = Context,
|
||||
Mitigations = Mitigations
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of release recording.
|
||||
/// </summary>
|
||||
public sealed record ReleaseRecordResult
|
||||
{
|
||||
public required bool IsSuccess { get; init; }
|
||||
public string? ReleaseId { get; init; }
|
||||
public int ConsumedRiskPoints { get; init; }
|
||||
public RiskBudget? Budget { get; init; }
|
||||
public GateLevel? Gate { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for exception recording.
|
||||
/// </summary>
|
||||
public sealed record ExceptionInput
|
||||
{
|
||||
public required string ReleaseId { get; init; }
|
||||
public required string ServiceId { get; init; }
|
||||
public required ServiceTier Tier { get; init; }
|
||||
public required DiffCategory DiffCategory { get; init; }
|
||||
public required OperationalContext Context { get; init; }
|
||||
public required MitigationFactors Mitigations { get; init; }
|
||||
public required string Reason { get; init; }
|
||||
public required string ApprovedBy { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of exception recording.
|
||||
/// </summary>
|
||||
public sealed record ExceptionResult
|
||||
{
|
||||
public required bool IsSuccess { get; init; }
|
||||
public required string ReleaseId { get; init; }
|
||||
public required int BaseRiskPoints { get; init; }
|
||||
public required int PenaltyRiskPoints { get; init; }
|
||||
public required int TotalRiskPoints { get; init; }
|
||||
public required RiskBudget Budget { get; init; }
|
||||
public required bool FollowUpRequired { get; init; }
|
||||
public DateTimeOffset? FollowUpDeadline { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for budget constraint enforcement.
|
||||
/// </summary>
|
||||
public interface IBudgetConstraintEnforcer
|
||||
{
|
||||
Task<BudgetCheckResult> CheckReleaseAsync(ReleaseCheckInput input, CancellationToken ct = default);
|
||||
Task<ReleaseRecordResult> RecordReleaseAsync(ReleaseRecordInput input, CancellationToken ct = default);
|
||||
Task<ExceptionResult> RecordExceptionAsync(ExceptionInput input, CancellationToken ct = default);
|
||||
}
|
||||
278
src/Policy/__Libraries/StellaOps.Policy/Gates/BudgetLedger.cs
Normal file
278
src/Policy/__Libraries/StellaOps.Policy/Gates/BudgetLedger.cs
Normal file
@@ -0,0 +1,278 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Ledger for tracking risk budget consumption.
|
||||
/// </summary>
|
||||
public sealed class BudgetLedger : IBudgetLedger
|
||||
{
|
||||
private readonly IBudgetStore _store;
|
||||
private readonly ILogger<BudgetLedger> _logger;
|
||||
|
||||
public BudgetLedger(IBudgetStore store, ILogger<BudgetLedger>? logger = null)
|
||||
{
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_logger = logger ?? NullLogger<BudgetLedger>.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current budget for a service.
|
||||
/// </summary>
|
||||
public async Task<RiskBudget> GetBudgetAsync(
|
||||
string serviceId,
|
||||
string? window = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
window ??= GetCurrentWindow();
|
||||
|
||||
var budget = await _store.GetAsync(serviceId, window, ct).ConfigureAwait(false);
|
||||
if (budget is not null)
|
||||
return budget;
|
||||
|
||||
// Create default budget if none exists
|
||||
var tier = await GetServiceTierAsync(serviceId, ct).ConfigureAwait(false);
|
||||
return await CreateBudgetAsync(serviceId, tier, window, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records consumption of risk points.
|
||||
/// </summary>
|
||||
public async Task<BudgetConsumeResult> ConsumeAsync(
|
||||
string serviceId,
|
||||
int riskPoints,
|
||||
string releaseId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var budget = await GetBudgetAsync(serviceId, ct: ct).ConfigureAwait(false);
|
||||
|
||||
if (budget.Remaining < riskPoints)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Budget exceeded for {ServiceId}: {Remaining} remaining, {Requested} requested",
|
||||
serviceId, budget.Remaining, riskPoints);
|
||||
|
||||
return new BudgetConsumeResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Budget = budget,
|
||||
Error = "Insufficient budget remaining"
|
||||
};
|
||||
}
|
||||
|
||||
// Record the consumption
|
||||
var entry = new BudgetEntry
|
||||
{
|
||||
EntryId = Guid.NewGuid().ToString(),
|
||||
ServiceId = serviceId,
|
||||
Window = budget.Window,
|
||||
ReleaseId = releaseId,
|
||||
RiskPoints = riskPoints,
|
||||
ConsumedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await _store.AddEntryAsync(entry, ct).ConfigureAwait(false);
|
||||
|
||||
// Update budget
|
||||
var updatedBudget = budget with
|
||||
{
|
||||
Consumed = budget.Consumed + riskPoints,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await _store.UpdateAsync(updatedBudget, ct).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Consumed {RiskPoints} RP for {ServiceId}. Remaining: {Remaining}/{Allocated}",
|
||||
riskPoints, serviceId, updatedBudget.Remaining, updatedBudget.Allocated);
|
||||
|
||||
return new BudgetConsumeResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
Budget = updatedBudget,
|
||||
Entry = entry
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the consumption history for a service.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BudgetEntry>> GetHistoryAsync(
|
||||
string serviceId,
|
||||
string? window = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
window ??= GetCurrentWindow();
|
||||
return await _store.GetEntriesAsync(serviceId, window, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts budget allocation (e.g., for earned capacity).
|
||||
/// </summary>
|
||||
public async Task<RiskBudget> AdjustAllocationAsync(
|
||||
string serviceId,
|
||||
int adjustment,
|
||||
string reason,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var budget = await GetBudgetAsync(serviceId, ct: ct).ConfigureAwait(false);
|
||||
|
||||
var newAllocation = Math.Max(0, budget.Allocated + adjustment);
|
||||
var updatedBudget = budget with
|
||||
{
|
||||
Allocated = newAllocation,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await _store.UpdateAsync(updatedBudget, ct).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Adjusted budget for {ServiceId} by {Adjustment} RP. Reason: {Reason}",
|
||||
serviceId, adjustment, reason);
|
||||
|
||||
return updatedBudget;
|
||||
}
|
||||
|
||||
private async Task<RiskBudget> CreateBudgetAsync(
|
||||
string serviceId,
|
||||
ServiceTier tier,
|
||||
string window,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var budget = new RiskBudget
|
||||
{
|
||||
BudgetId = $"budget:{serviceId}:{window}",
|
||||
ServiceId = serviceId,
|
||||
Tier = tier,
|
||||
Window = window,
|
||||
Allocated = DefaultBudgetAllocations.GetMonthlyAllocation(tier),
|
||||
Consumed = 0,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await _store.CreateAsync(budget, ct).ConfigureAwait(false);
|
||||
return budget;
|
||||
}
|
||||
|
||||
private static string GetCurrentWindow() =>
|
||||
DateTimeOffset.UtcNow.ToString("yyyy-MM");
|
||||
|
||||
private Task<ServiceTier> GetServiceTierAsync(string serviceId, CancellationToken ct)
|
||||
{
|
||||
// Look up service tier from configuration or default to Tier 1
|
||||
return Task.FromResult(ServiceTier.CustomerFacingNonCritical);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry recording a budget consumption.
|
||||
/// </summary>
|
||||
public sealed record BudgetEntry
|
||||
{
|
||||
public required string EntryId { get; init; }
|
||||
public required string ServiceId { get; init; }
|
||||
public required string Window { get; init; }
|
||||
public required string ReleaseId { get; init; }
|
||||
public required int RiskPoints { get; init; }
|
||||
public required DateTimeOffset ConsumedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of budget consumption attempt.
|
||||
/// </summary>
|
||||
public sealed record BudgetConsumeResult
|
||||
{
|
||||
public required bool IsSuccess { get; init; }
|
||||
public required RiskBudget Budget { get; init; }
|
||||
public BudgetEntry? Entry { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for budget ledger operations.
|
||||
/// </summary>
|
||||
public interface IBudgetLedger
|
||||
{
|
||||
Task<RiskBudget> GetBudgetAsync(string serviceId, string? window = null, CancellationToken ct = default);
|
||||
Task<BudgetConsumeResult> ConsumeAsync(string serviceId, int riskPoints, string releaseId, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<BudgetEntry>> GetHistoryAsync(string serviceId, string? window = null, CancellationToken ct = default);
|
||||
Task<RiskBudget> AdjustAllocationAsync(string serviceId, int adjustment, string reason, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for budget persistence.
|
||||
/// </summary>
|
||||
public interface IBudgetStore
|
||||
{
|
||||
Task<RiskBudget?> GetAsync(string serviceId, string window, CancellationToken ct);
|
||||
Task CreateAsync(RiskBudget budget, CancellationToken ct);
|
||||
Task UpdateAsync(RiskBudget budget, CancellationToken ct);
|
||||
Task AddEntryAsync(BudgetEntry entry, CancellationToken ct);
|
||||
Task<IReadOnlyList<BudgetEntry>> GetEntriesAsync(string serviceId, string window, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of <see cref="IBudgetStore"/> for testing.
|
||||
/// </summary>
|
||||
public sealed class InMemoryBudgetStore : IBudgetStore
|
||||
{
|
||||
private readonly Dictionary<string, RiskBudget> _budgets = new();
|
||||
private readonly List<BudgetEntry> _entries = [];
|
||||
private readonly object _lock = new();
|
||||
|
||||
public Task<RiskBudget?> GetAsync(string serviceId, string window, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var key = $"{serviceId}:{window}";
|
||||
lock (_lock)
|
||||
{
|
||||
return Task.FromResult(_budgets.TryGetValue(key, out var budget) ? budget : null);
|
||||
}
|
||||
}
|
||||
|
||||
public Task CreateAsync(RiskBudget budget, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var key = $"{budget.ServiceId}:{budget.Window}";
|
||||
lock (_lock)
|
||||
{
|
||||
_budgets[key] = budget;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UpdateAsync(RiskBudget budget, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var key = $"{budget.ServiceId}:{budget.Window}";
|
||||
lock (_lock)
|
||||
{
|
||||
_budgets[key] = budget;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AddEntryAsync(BudgetEntry entry, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
lock (_lock)
|
||||
{
|
||||
_entries.Add(entry);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<BudgetEntry>> GetEntriesAsync(string serviceId, string window, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
lock (_lock)
|
||||
{
|
||||
var result = _entries
|
||||
.Where(e => e.ServiceId == serviceId && e.Window == window)
|
||||
.OrderByDescending(e => e.ConsumedAt)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<BudgetEntry>>(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/Policy/__Libraries/StellaOps.Policy/Gates/GateLevel.cs
Normal file
122
src/Policy/__Libraries/StellaOps.Policy/Gates/GateLevel.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
namespace StellaOps.Policy.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Diff-aware release gate levels (G0-G4).
|
||||
/// Higher levels require more checks before release.
|
||||
/// </summary>
|
||||
public enum GateLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// G0: No-risk / Administrative.
|
||||
/// Requirements: Lint/format checks, basic CI pass.
|
||||
/// Use for: docs-only, comments-only, non-functional metadata.
|
||||
/// </summary>
|
||||
G0 = 0,
|
||||
|
||||
/// <summary>
|
||||
/// G1: Low risk.
|
||||
/// Requirements: All automated unit tests, static analysis, 1 peer review, staging deploy, smoke checks.
|
||||
/// Use for: small localized changes, non-core UI, telemetry additions.
|
||||
/// </summary>
|
||||
G1 = 1,
|
||||
|
||||
/// <summary>
|
||||
/// G2: Moderate risk.
|
||||
/// Requirements: G1 + integration tests, code owner review, feature flag required, staged rollout, rollback plan.
|
||||
/// Use for: moderate logic changes, dependency upgrades, backward-compatible API changes.
|
||||
/// </summary>
|
||||
G2 = 2,
|
||||
|
||||
/// <summary>
|
||||
/// G3: High risk.
|
||||
/// Requirements: G2 + security scan, migration plan reviewed, load/performance checks, observability updates, release captain sign-off, progressive delivery with health gates.
|
||||
/// Use for: schema migrations, auth/permission changes, core business logic, infra changes.
|
||||
/// </summary>
|
||||
G3 = 3,
|
||||
|
||||
/// <summary>
|
||||
/// G4: Very high risk / Safety-critical.
|
||||
/// Requirements: G3 + formal risk review (PM+DM+Security), rollback rehearsal, extended canary, customer comms plan, post-release verification checklist.
|
||||
/// Use for: Tier 3 systems with low budget, freeze window exceptions, platform-wide changes.
|
||||
/// </summary>
|
||||
G4 = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gate level requirements documentation.
|
||||
/// </summary>
|
||||
public static class GateLevelRequirements
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the requirements for a gate level.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> GetRequirements(GateLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
GateLevel.G0 =>
|
||||
[
|
||||
"Lint/format checks pass",
|
||||
"Basic CI build passes"
|
||||
],
|
||||
|
||||
GateLevel.G1 =>
|
||||
[
|
||||
"All automated unit tests pass",
|
||||
"Static analysis/linting clean",
|
||||
"1 peer review (code owner not required)",
|
||||
"Automated deploy to staging",
|
||||
"Post-deploy smoke checks pass"
|
||||
],
|
||||
|
||||
GateLevel.G2 =>
|
||||
[
|
||||
"All G1 requirements",
|
||||
"Integration tests for impacted modules pass",
|
||||
"Code owner review for touched modules",
|
||||
"Feature flag required if customer impact possible",
|
||||
"Staged rollout: canary or small cohort",
|
||||
"Rollback plan documented in PR"
|
||||
],
|
||||
|
||||
GateLevel.G3 =>
|
||||
[
|
||||
"All G2 requirements",
|
||||
"Security scan + dependency audit pass",
|
||||
"Migration plan (forward + rollback) reviewed",
|
||||
"Load/performance checks if in hot path",
|
||||
"Observability: new/updated dashboards/alerts",
|
||||
"Release captain / on-call sign-off",
|
||||
"Progressive delivery with automatic health gates"
|
||||
],
|
||||
|
||||
GateLevel.G4 =>
|
||||
[
|
||||
"All G3 requirements",
|
||||
"Formal risk review (PM+DM+Security/SRE) in writing",
|
||||
"Explicit rollback rehearsal or proven rollback path",
|
||||
"Extended canary period with success/abort criteria",
|
||||
"Customer comms plan if impact is plausible",
|
||||
"Post-release verification checklist executed and logged"
|
||||
],
|
||||
|
||||
_ => []
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a short description for a gate level.
|
||||
/// </summary>
|
||||
public static string GetDescription(GateLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
GateLevel.G0 => "No-risk: Basic CI only",
|
||||
GateLevel.G1 => "Low risk: Unit tests + 1 review",
|
||||
GateLevel.G2 => "Moderate risk: Integration tests + code owner + canary",
|
||||
GateLevel.G3 => "High risk: Security scan + release captain + progressive",
|
||||
GateLevel.G4 => "Very high risk: Formal review + extended canary + comms",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
}
|
||||
175
src/Policy/__Libraries/StellaOps.Policy/Gates/GateSelector.cs
Normal file
175
src/Policy/__Libraries/StellaOps.Policy/Gates/GateSelector.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Selects the appropriate gate level for a release.
|
||||
/// </summary>
|
||||
public sealed class GateSelector : IGateSelector
|
||||
{
|
||||
private readonly IRiskPointScoring _scoring;
|
||||
private readonly IBudgetLedger _budgetLedger;
|
||||
private readonly ILogger<GateSelector> _logger;
|
||||
|
||||
public GateSelector(
|
||||
IRiskPointScoring scoring,
|
||||
IBudgetLedger budgetLedger,
|
||||
ILogger<GateSelector>? logger = null)
|
||||
{
|
||||
_scoring = scoring ?? throw new ArgumentNullException(nameof(scoring));
|
||||
_budgetLedger = budgetLedger ?? throw new ArgumentNullException(nameof(budgetLedger));
|
||||
_logger = logger ?? NullLogger<GateSelector>.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the gate level for a change.
|
||||
/// </summary>
|
||||
public async Task<GateSelectionResult> SelectGateAsync(
|
||||
GateSelectionInput input,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Get current budget status
|
||||
var budget = await _budgetLedger.GetBudgetAsync(input.ServiceId, ct: ct).ConfigureAwait(false);
|
||||
|
||||
// Build context with budget status
|
||||
var context = input.Context with { BudgetStatus = budget.Status };
|
||||
|
||||
// Calculate risk score
|
||||
var scoreInput = new RiskScoreInput
|
||||
{
|
||||
Tier = input.Tier,
|
||||
DiffCategory = input.DiffCategory,
|
||||
Context = context,
|
||||
Mitigations = input.Mitigations
|
||||
};
|
||||
|
||||
var scoreResult = _scoring.CalculateScore(scoreInput);
|
||||
|
||||
// Apply budget-based modifiers
|
||||
var finalGate = ApplyBudgetModifiers(scoreResult.RecommendedGate, budget);
|
||||
|
||||
// Check for blocks
|
||||
var (isBlocked, blockReason) = CheckForBlocks(finalGate, budget, input);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Gate selection for {ServiceId}: Score={Score}, Gate={Gate}, Budget={BudgetStatus}",
|
||||
input.ServiceId, scoreResult.Score, finalGate, budget.Status);
|
||||
|
||||
return new GateSelectionResult
|
||||
{
|
||||
Gate = finalGate,
|
||||
RiskScore = scoreResult.Score,
|
||||
ScoreBreakdown = scoreResult.Breakdown,
|
||||
Budget = budget,
|
||||
IsBlocked = isBlocked,
|
||||
BlockReason = blockReason,
|
||||
Requirements = GateLevelRequirements.GetRequirements(finalGate).ToList(),
|
||||
Recommendations = GenerateRecommendations(scoreResult, budget)
|
||||
};
|
||||
}
|
||||
|
||||
private static GateLevel ApplyBudgetModifiers(GateLevel gate, RiskBudget budget)
|
||||
{
|
||||
return budget.Status switch
|
||||
{
|
||||
// Yellow: Escalate G2+ by one level
|
||||
BudgetStatus.Yellow when gate >= GateLevel.G2 =>
|
||||
gate < GateLevel.G4 ? gate + 1 : GateLevel.G4,
|
||||
|
||||
// Red: Escalate G1+ by one level
|
||||
BudgetStatus.Red when gate >= GateLevel.G1 =>
|
||||
gate < GateLevel.G4 ? gate + 1 : GateLevel.G4,
|
||||
|
||||
// Exhausted: Everything is G4
|
||||
BudgetStatus.Exhausted => GateLevel.G4,
|
||||
|
||||
_ => gate
|
||||
};
|
||||
}
|
||||
|
||||
private static (bool IsBlocked, string? Reason) CheckForBlocks(
|
||||
GateLevel gate, RiskBudget budget, GateSelectionInput input)
|
||||
{
|
||||
// Red budget blocks high-risk categories
|
||||
if (budget.Status == BudgetStatus.Red &&
|
||||
input.DiffCategory is DiffCategory.DatabaseMigration or DiffCategory.AuthPermission or DiffCategory.InfraNetworking)
|
||||
{
|
||||
return (true, "High-risk changes blocked during Red budget status");
|
||||
}
|
||||
|
||||
// Exhausted budget blocks non-emergency changes
|
||||
if (budget.Status == BudgetStatus.Exhausted && !input.IsEmergencyFix)
|
||||
{
|
||||
return (true, "Budget exhausted. Only incident/security fixes allowed.");
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GenerateRecommendations(
|
||||
RiskScoreResult score, RiskBudget budget)
|
||||
{
|
||||
var recommendations = new List<string>();
|
||||
|
||||
// Score reduction recommendations
|
||||
if (score.Breakdown.DiffRisk > 5)
|
||||
{
|
||||
recommendations.Add("Consider breaking this change into smaller, lower-risk diffs");
|
||||
}
|
||||
|
||||
if (score.Breakdown.Mitigations == 0)
|
||||
{
|
||||
recommendations.Add("Add mitigations: feature flag, canary deployment, or increased test coverage");
|
||||
}
|
||||
|
||||
// Budget recommendations
|
||||
if (budget.Status == BudgetStatus.Yellow)
|
||||
{
|
||||
recommendations.Add("Budget at Yellow status. Prioritize reliability work to restore capacity.");
|
||||
}
|
||||
|
||||
if (budget.Status == BudgetStatus.Red)
|
||||
{
|
||||
recommendations.Add("Budget at Red status. Defer high-risk changes or decompose into smaller diffs.");
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for gate selection.
|
||||
/// </summary>
|
||||
public sealed record GateSelectionInput
|
||||
{
|
||||
public required string ServiceId { get; init; }
|
||||
public required ServiceTier Tier { get; init; }
|
||||
public required DiffCategory DiffCategory { get; init; }
|
||||
public required OperationalContext Context { get; init; }
|
||||
public required MitigationFactors Mitigations { get; init; }
|
||||
public bool IsEmergencyFix { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of gate selection.
|
||||
/// </summary>
|
||||
public sealed record GateSelectionResult
|
||||
{
|
||||
public required GateLevel Gate { get; init; }
|
||||
public required int RiskScore { get; init; }
|
||||
public required RiskScoreBreakdown ScoreBreakdown { get; init; }
|
||||
public required RiskBudget Budget { get; init; }
|
||||
public required bool IsBlocked { get; init; }
|
||||
public string? BlockReason { get; init; }
|
||||
public IReadOnlyList<string> Requirements { get; init; } = [];
|
||||
public IReadOnlyList<string> Recommendations { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for gate selection.
|
||||
/// </summary>
|
||||
public interface IGateSelector
|
||||
{
|
||||
Task<GateSelectionResult> SelectGateAsync(GateSelectionInput input, CancellationToken ct = default);
|
||||
}
|
||||
136
src/Policy/__Libraries/StellaOps.Policy/Gates/RiskBudget.cs
Normal file
136
src/Policy/__Libraries/StellaOps.Policy/Gates/RiskBudget.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
namespace StellaOps.Policy.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a risk budget for a service/product.
|
||||
/// Tracks risk point allocation and consumption.
|
||||
/// </summary>
|
||||
public sealed record RiskBudget
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this budget.
|
||||
/// </summary>
|
||||
public required string BudgetId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Service or product this budget applies to.
|
||||
/// </summary>
|
||||
public required string ServiceId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Criticality tier (0-3).
|
||||
/// </summary>
|
||||
public required ServiceTier Tier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Budget window (e.g., "2025-01" for monthly).
|
||||
/// </summary>
|
||||
public required string Window { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total risk points allocated for this window.
|
||||
/// </summary>
|
||||
public required int Allocated { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk points consumed so far.
|
||||
/// </summary>
|
||||
public int Consumed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk points remaining.
|
||||
/// </summary>
|
||||
public int Remaining => Allocated - Consumed;
|
||||
|
||||
/// <summary>
|
||||
/// Percentage of budget used.
|
||||
/// </summary>
|
||||
public decimal PercentageUsed => Allocated > 0
|
||||
? (decimal)Consumed / Allocated * 100
|
||||
: 0;
|
||||
|
||||
/// <summary>
|
||||
/// Current operating status.
|
||||
/// </summary>
|
||||
public BudgetStatus Status => PercentageUsed switch
|
||||
{
|
||||
< 40 => BudgetStatus.Green,
|
||||
< 70 => BudgetStatus.Yellow,
|
||||
< 100 => BudgetStatus.Red,
|
||||
_ => BudgetStatus.Exhausted
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Last updated timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service criticality tiers.
|
||||
/// </summary>
|
||||
public enum ServiceTier
|
||||
{
|
||||
/// <summary>
|
||||
/// Tier 0: Internal only, low business impact.
|
||||
/// </summary>
|
||||
Internal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Tier 1: Customer-facing non-critical.
|
||||
/// </summary>
|
||||
CustomerFacingNonCritical = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Tier 2: Customer-facing critical.
|
||||
/// </summary>
|
||||
CustomerFacingCritical = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Tier 3: Safety/financial/data-critical.
|
||||
/// </summary>
|
||||
SafetyCritical = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Budget operating status.
|
||||
/// </summary>
|
||||
public enum BudgetStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Green: >= 60% remaining. Normal operation.
|
||||
/// </summary>
|
||||
Green,
|
||||
|
||||
/// <summary>
|
||||
/// Yellow: 30-59% remaining. Increased caution.
|
||||
/// </summary>
|
||||
Yellow,
|
||||
|
||||
/// <summary>
|
||||
/// Red: Less than 30% remaining. Freeze high-risk diffs.
|
||||
/// </summary>
|
||||
Red,
|
||||
|
||||
/// <summary>
|
||||
/// Exhausted: 0% or less remaining. Incident/security fixes only.
|
||||
/// </summary>
|
||||
Exhausted
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default budget allocations by tier.
|
||||
/// </summary>
|
||||
public static class DefaultBudgetAllocations
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default monthly allocation for a service tier.
|
||||
/// </summary>
|
||||
public static int GetMonthlyAllocation(ServiceTier tier) => tier switch
|
||||
{
|
||||
ServiceTier.Internal => 300,
|
||||
ServiceTier.CustomerFacingNonCritical => 200,
|
||||
ServiceTier.CustomerFacingCritical => 120,
|
||||
ServiceTier.SafetyCritical => 80,
|
||||
_ => 100
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Policy.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Release Risk Score (RRS) for changes.
|
||||
/// RRS = Base(criticality) + Diff Risk + Operational Context - Mitigations
|
||||
/// </summary>
|
||||
public sealed class RiskPointScoring : IRiskPointScoring
|
||||
{
|
||||
private readonly RiskScoringOptions _options;
|
||||
|
||||
public RiskPointScoring(IOptionsMonitor<RiskScoringOptions>? options = null)
|
||||
{
|
||||
_options = options?.CurrentValue ?? RiskScoringOptions.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the Release Risk Score for a change.
|
||||
/// </summary>
|
||||
public RiskScoreResult CalculateScore(RiskScoreInput input)
|
||||
{
|
||||
var breakdown = new RiskScoreBreakdown();
|
||||
|
||||
// Base score from service tier
|
||||
var baseScore = GetBaseScore(input.Tier);
|
||||
breakdown.Base = baseScore;
|
||||
|
||||
// Diff risk (additive)
|
||||
var diffRisk = CalculateDiffRisk(input.DiffCategory);
|
||||
breakdown.DiffRisk = diffRisk;
|
||||
|
||||
// Operational context (additive)
|
||||
var operationalContext = CalculateOperationalContext(input.Context);
|
||||
breakdown.OperationalContext = operationalContext;
|
||||
|
||||
// Mitigations (subtract)
|
||||
var mitigations = CalculateMitigations(input.Mitigations);
|
||||
breakdown.Mitigations = mitigations;
|
||||
|
||||
// Total (minimum 1)
|
||||
var total = Math.Max(1, baseScore + diffRisk + operationalContext - mitigations);
|
||||
breakdown.Total = total;
|
||||
|
||||
// Determine gate level
|
||||
var gate = DetermineGateLevel(total, input.Context.BudgetStatus);
|
||||
|
||||
return new RiskScoreResult
|
||||
{
|
||||
Score = total,
|
||||
Breakdown = breakdown,
|
||||
RecommendedGate = gate
|
||||
};
|
||||
}
|
||||
|
||||
private int GetBaseScore(ServiceTier tier)
|
||||
{
|
||||
return tier switch
|
||||
{
|
||||
ServiceTier.Internal => _options.BaseScores.Tier0,
|
||||
ServiceTier.CustomerFacingNonCritical => _options.BaseScores.Tier1,
|
||||
ServiceTier.CustomerFacingCritical => _options.BaseScores.Tier2,
|
||||
ServiceTier.SafetyCritical => _options.BaseScores.Tier3,
|
||||
_ => 1
|
||||
};
|
||||
}
|
||||
|
||||
private static int CalculateDiffRisk(DiffCategory category)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
DiffCategory.DocsOnly => 1,
|
||||
DiffCategory.UiNonCore => 3,
|
||||
DiffCategory.ApiBackwardCompatible => 6,
|
||||
DiffCategory.ApiBreaking => 12,
|
||||
DiffCategory.DatabaseMigration => 10,
|
||||
DiffCategory.AuthPermission => 10,
|
||||
DiffCategory.InfraNetworking => 15,
|
||||
DiffCategory.CryptoPayment => 15,
|
||||
DiffCategory.Other => 3,
|
||||
_ => 3
|
||||
};
|
||||
}
|
||||
|
||||
private static int CalculateOperationalContext(OperationalContext context)
|
||||
{
|
||||
var score = 0;
|
||||
|
||||
if (context.HasRecentIncident)
|
||||
score += 5;
|
||||
|
||||
if (context.ErrorBudgetBelow50Percent)
|
||||
score += 3;
|
||||
|
||||
if (context.HighOnCallLoad)
|
||||
score += 2;
|
||||
|
||||
if (context.InRestrictedWindow)
|
||||
score += 5;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static int CalculateMitigations(MitigationFactors mitigations)
|
||||
{
|
||||
var reduction = 0;
|
||||
|
||||
if (mitigations.HasFeatureFlag)
|
||||
reduction += 3;
|
||||
|
||||
if (mitigations.HasCanaryDeployment)
|
||||
reduction += 3;
|
||||
|
||||
if (mitigations.HasHighTestCoverage)
|
||||
reduction += 2;
|
||||
|
||||
if (mitigations.HasBackwardCompatibleMigration)
|
||||
reduction += 2;
|
||||
|
||||
if (mitigations.HasPermissionBoundary)
|
||||
reduction += 2;
|
||||
|
||||
return reduction;
|
||||
}
|
||||
|
||||
private static GateLevel DetermineGateLevel(int score, BudgetStatus budgetStatus)
|
||||
{
|
||||
var baseGate = score switch
|
||||
{
|
||||
<= 5 => GateLevel.G1,
|
||||
<= 12 => GateLevel.G2,
|
||||
<= 20 => GateLevel.G3,
|
||||
_ => GateLevel.G4
|
||||
};
|
||||
|
||||
// Escalate based on budget status
|
||||
return budgetStatus switch
|
||||
{
|
||||
BudgetStatus.Yellow when baseGate >= GateLevel.G2 => EscalateGate(baseGate),
|
||||
BudgetStatus.Red when baseGate >= GateLevel.G1 => EscalateGate(baseGate),
|
||||
BudgetStatus.Exhausted => GateLevel.G4,
|
||||
_ => baseGate
|
||||
};
|
||||
}
|
||||
|
||||
private static GateLevel EscalateGate(GateLevel gate) =>
|
||||
gate < GateLevel.G4 ? gate + 1 : GateLevel.G4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for risk score calculation.
|
||||
/// </summary>
|
||||
public sealed record RiskScoreInput
|
||||
{
|
||||
public required ServiceTier Tier { get; init; }
|
||||
public required DiffCategory DiffCategory { get; init; }
|
||||
public required OperationalContext Context { get; init; }
|
||||
public required MitigationFactors Mitigations { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Categories of diffs affecting risk score.
|
||||
/// </summary>
|
||||
public enum DiffCategory
|
||||
{
|
||||
DocsOnly,
|
||||
UiNonCore,
|
||||
ApiBackwardCompatible,
|
||||
ApiBreaking,
|
||||
DatabaseMigration,
|
||||
AuthPermission,
|
||||
InfraNetworking,
|
||||
CryptoPayment,
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Operational context affecting risk.
|
||||
/// </summary>
|
||||
public sealed record OperationalContext
|
||||
{
|
||||
public bool HasRecentIncident { get; init; }
|
||||
public bool ErrorBudgetBelow50Percent { get; init; }
|
||||
public bool HighOnCallLoad { get; init; }
|
||||
public bool InRestrictedWindow { get; init; }
|
||||
public BudgetStatus BudgetStatus { get; init; }
|
||||
|
||||
public static OperationalContext Default { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mitigation factors that reduce risk.
|
||||
/// </summary>
|
||||
public sealed record MitigationFactors
|
||||
{
|
||||
public bool HasFeatureFlag { get; init; }
|
||||
public bool HasCanaryDeployment { get; init; }
|
||||
public bool HasHighTestCoverage { get; init; }
|
||||
public bool HasBackwardCompatibleMigration { get; init; }
|
||||
public bool HasPermissionBoundary { get; init; }
|
||||
|
||||
public static MitigationFactors None { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of risk score calculation.
|
||||
/// </summary>
|
||||
public sealed record RiskScoreResult
|
||||
{
|
||||
public required int Score { get; init; }
|
||||
public required RiskScoreBreakdown Breakdown { get; init; }
|
||||
public required GateLevel RecommendedGate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of score components.
|
||||
/// </summary>
|
||||
public sealed record RiskScoreBreakdown
|
||||
{
|
||||
public int Base { get; set; }
|
||||
public int DiffRisk { get; set; }
|
||||
public int OperationalContext { get; set; }
|
||||
public int Mitigations { get; set; }
|
||||
public int Total { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for risk scoring.
|
||||
/// </summary>
|
||||
public sealed record RiskScoringOptions
|
||||
{
|
||||
public BaseScoresByTier BaseScores { get; init; } = new();
|
||||
|
||||
public static RiskScoringOptions Default { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base scores by service tier.
|
||||
/// </summary>
|
||||
public sealed record BaseScoresByTier
|
||||
{
|
||||
public int Tier0 { get; init; } = 1;
|
||||
public int Tier1 { get; init; } = 3;
|
||||
public int Tier2 { get; init; } = 6;
|
||||
public int Tier3 { get; init; } = 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for risk point scoring.
|
||||
/// </summary>
|
||||
public interface IRiskPointScoring
|
||||
{
|
||||
RiskScoreResult CalculateScore(RiskScoreInput input);
|
||||
}
|
||||
Reference in New Issue
Block a user