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,123 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Gates;
|
||||
|
||||
public sealed class BudgetLedgerTests
|
||||
{
|
||||
private readonly InMemoryBudgetStore _store = new();
|
||||
private readonly BudgetLedger _ledger;
|
||||
private readonly string _currentWindow;
|
||||
|
||||
public BudgetLedgerTests()
|
||||
{
|
||||
_ledger = new BudgetLedger(_store);
|
||||
_currentWindow = DateTimeOffset.UtcNow.ToString("yyyy-MM");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBudget_CreatesDefaultWhenNotExists()
|
||||
{
|
||||
var budget = await _ledger.GetBudgetAsync("new-service");
|
||||
|
||||
budget.Should().NotBeNull();
|
||||
budget.ServiceId.Should().Be("new-service");
|
||||
budget.Tier.Should().Be(ServiceTier.CustomerFacingNonCritical);
|
||||
budget.Allocated.Should().Be(200); // Default for Tier 1
|
||||
budget.Consumed.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBudget_ReturnsExistingBudget()
|
||||
{
|
||||
var existing = CreateBudget("existing-service", consumed: 50);
|
||||
await _store.CreateAsync(existing, CancellationToken.None);
|
||||
|
||||
var budget = await _ledger.GetBudgetAsync("existing-service", _currentWindow);
|
||||
|
||||
budget.Consumed.Should().Be(50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Consume_DeductsBudget()
|
||||
{
|
||||
var initial = CreateBudget("test-service", consumed: 50);
|
||||
await _store.CreateAsync(initial, CancellationToken.None);
|
||||
|
||||
var result = await _ledger.ConsumeAsync("test-service", 20, "release-1");
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Budget.Consumed.Should().Be(70);
|
||||
result.Budget.Remaining.Should().Be(130);
|
||||
result.Entry.Should().NotBeNull();
|
||||
result.Entry!.RiskPoints.Should().Be(20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Consume_FailsWhenInsufficientBudget()
|
||||
{
|
||||
var initial = CreateBudget("test-service", consumed: 190);
|
||||
await _store.CreateAsync(initial, CancellationToken.None);
|
||||
|
||||
var result = await _ledger.ConsumeAsync("test-service", 20, "release-1");
|
||||
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Error.Should().Contain("Insufficient");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHistory_ReturnsEntries()
|
||||
{
|
||||
await _ledger.GetBudgetAsync("test-service");
|
||||
await _ledger.ConsumeAsync("test-service", 10, "release-1");
|
||||
await _ledger.ConsumeAsync("test-service", 15, "release-2");
|
||||
|
||||
var history = await _ledger.GetHistoryAsync("test-service");
|
||||
|
||||
history.Should().HaveCount(2);
|
||||
history.Should().Contain(e => e.ReleaseId == "release-1");
|
||||
history.Should().Contain(e => e.ReleaseId == "release-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AdjustAllocation_IncreasesCapacity()
|
||||
{
|
||||
await _ledger.GetBudgetAsync("test-service");
|
||||
|
||||
var adjusted = await _ledger.AdjustAllocationAsync("test-service", 50, "earned capacity");
|
||||
|
||||
adjusted.Allocated.Should().Be(250); // 200 + 50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AdjustAllocation_DecreasesCapacity()
|
||||
{
|
||||
await _ledger.GetBudgetAsync("test-service");
|
||||
|
||||
var adjusted = await _ledger.AdjustAllocationAsync("test-service", -50, "incident penalty");
|
||||
|
||||
adjusted.Allocated.Should().Be(150); // 200 - 50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AdjustAllocation_DoesNotGoBelowZero()
|
||||
{
|
||||
await _ledger.GetBudgetAsync("test-service");
|
||||
|
||||
var adjusted = await _ledger.AdjustAllocationAsync("test-service", -500, "major penalty");
|
||||
|
||||
adjusted.Allocated.Should().Be(0);
|
||||
}
|
||||
|
||||
private RiskBudget CreateBudget(string serviceId, int consumed) => new()
|
||||
{
|
||||
BudgetId = $"budget:{serviceId}:{_currentWindow}",
|
||||
ServiceId = serviceId,
|
||||
Tier = ServiceTier.CustomerFacingNonCritical,
|
||||
Window = _currentWindow,
|
||||
Allocated = 200,
|
||||
Consumed = consumed,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Gates;
|
||||
|
||||
public sealed class GateLevelTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(GateLevel.G0, 2)]
|
||||
[InlineData(GateLevel.G1, 5)]
|
||||
[InlineData(GateLevel.G2, 6)]
|
||||
[InlineData(GateLevel.G3, 7)]
|
||||
[InlineData(GateLevel.G4, 6)]
|
||||
public void GetRequirements_ReturnsCorrectCount(GateLevel level, int expectedCount)
|
||||
{
|
||||
var requirements = GateLevelRequirements.GetRequirements(level);
|
||||
requirements.Should().HaveCount(expectedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRequirements_G0_HasBasicCiOnly()
|
||||
{
|
||||
var requirements = GateLevelRequirements.GetRequirements(GateLevel.G0);
|
||||
|
||||
requirements.Should().Contain(r => r.Contains("Lint"));
|
||||
requirements.Should().Contain(r => r.Contains("CI"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRequirements_G1_HasUnitTestsAndReview()
|
||||
{
|
||||
var requirements = GateLevelRequirements.GetRequirements(GateLevel.G1);
|
||||
|
||||
requirements.Should().Contain(r => r.Contains("unit tests"));
|
||||
requirements.Should().Contain(r => r.Contains("peer review"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRequirements_G2_IncludesG1Requirements()
|
||||
{
|
||||
var requirements = GateLevelRequirements.GetRequirements(GateLevel.G2);
|
||||
|
||||
requirements.Should().Contain(r => r.Contains("G1"));
|
||||
requirements.Should().Contain(r => r.Contains("Code owner", StringComparison.OrdinalIgnoreCase));
|
||||
requirements.Should().Contain(r => r.Contains("feature flag", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRequirements_G3_HasSecurityAndReleaseSign()
|
||||
{
|
||||
var requirements = GateLevelRequirements.GetRequirements(GateLevel.G3);
|
||||
|
||||
requirements.Should().Contain(r => r.Contains("Security scan", StringComparison.OrdinalIgnoreCase));
|
||||
requirements.Should().Contain(r => r.Contains("release captain", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRequirements_G4_HasFormalReviewAndCanary()
|
||||
{
|
||||
var requirements = GateLevelRequirements.GetRequirements(GateLevel.G4);
|
||||
|
||||
requirements.Should().Contain(r => r.Contains("Formal risk review"));
|
||||
requirements.Should().Contain(r => r.Contains("Extended canary"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(GateLevel.G0, "No-risk")]
|
||||
[InlineData(GateLevel.G1, "Low risk")]
|
||||
[InlineData(GateLevel.G2, "Moderate risk")]
|
||||
[InlineData(GateLevel.G3, "High risk")]
|
||||
[InlineData(GateLevel.G4, "Very high risk")]
|
||||
public void GetDescription_ContainsExpectedText(GateLevel level, string expectedText)
|
||||
{
|
||||
var description = GateLevelRequirements.GetDescription(level);
|
||||
description.Should().Contain(expectedText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Gates;
|
||||
|
||||
public sealed class RiskBudgetTests
|
||||
{
|
||||
[Fact]
|
||||
public void Budget_WithNoConsumption_IsGreen()
|
||||
{
|
||||
var budget = CreateBudget(allocated: 200, consumed: 0);
|
||||
|
||||
budget.Status.Should().Be(BudgetStatus.Green);
|
||||
budget.Remaining.Should().Be(200);
|
||||
budget.PercentageUsed.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Budget_With30PercentUsed_IsGreen()
|
||||
{
|
||||
var budget = CreateBudget(allocated: 200, consumed: 60);
|
||||
|
||||
budget.Status.Should().Be(BudgetStatus.Green);
|
||||
budget.PercentageUsed.Should().Be(30);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Budget_With40PercentUsed_IsYellow()
|
||||
{
|
||||
var budget = CreateBudget(allocated: 200, consumed: 80);
|
||||
|
||||
budget.Status.Should().Be(BudgetStatus.Yellow);
|
||||
budget.PercentageUsed.Should().Be(40);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Budget_With70PercentUsed_IsRed()
|
||||
{
|
||||
var budget = CreateBudget(allocated: 200, consumed: 140);
|
||||
|
||||
budget.Status.Should().Be(BudgetStatus.Red);
|
||||
budget.PercentageUsed.Should().Be(70);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Budget_With100PercentUsed_IsExhausted()
|
||||
{
|
||||
var budget = CreateBudget(allocated: 200, consumed: 200);
|
||||
|
||||
budget.Status.Should().Be(BudgetStatus.Exhausted);
|
||||
budget.Remaining.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Budget_Overconsumed_IsExhausted()
|
||||
{
|
||||
var budget = CreateBudget(allocated: 200, consumed: 250);
|
||||
|
||||
budget.Status.Should().Be(BudgetStatus.Exhausted);
|
||||
budget.Remaining.Should().Be(-50);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ServiceTier.Internal, 300)]
|
||||
[InlineData(ServiceTier.CustomerFacingNonCritical, 200)]
|
||||
[InlineData(ServiceTier.CustomerFacingCritical, 120)]
|
||||
[InlineData(ServiceTier.SafetyCritical, 80)]
|
||||
public void DefaultAllocations_AreCorrect(ServiceTier tier, int expected)
|
||||
{
|
||||
var allocation = DefaultBudgetAllocations.GetMonthlyAllocation(tier);
|
||||
allocation.Should().Be(expected);
|
||||
}
|
||||
|
||||
private static RiskBudget CreateBudget(int allocated, int consumed) => new()
|
||||
{
|
||||
BudgetId = "budget:test:2025-01",
|
||||
ServiceId = "test-service",
|
||||
Tier = ServiceTier.CustomerFacingNonCritical,
|
||||
Window = "2025-01",
|
||||
Allocated = allocated,
|
||||
Consumed = consumed,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Gates;
|
||||
|
||||
public sealed class RiskPointScoringTests
|
||||
{
|
||||
private readonly RiskPointScoring _scoring = new();
|
||||
|
||||
[Theory]
|
||||
[InlineData(ServiceTier.Internal, 1)]
|
||||
[InlineData(ServiceTier.CustomerFacingNonCritical, 3)]
|
||||
[InlineData(ServiceTier.CustomerFacingCritical, 6)]
|
||||
[InlineData(ServiceTier.SafetyCritical, 10)]
|
||||
public void CalculateScore_UsesCorrectBaseScore(ServiceTier tier, int expectedBase)
|
||||
{
|
||||
var input = CreateInput(tier, DiffCategory.DocsOnly);
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
result.Breakdown.Base.Should().Be(expectedBase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(DiffCategory.DocsOnly, 1)]
|
||||
[InlineData(DiffCategory.UiNonCore, 3)]
|
||||
[InlineData(DiffCategory.ApiBackwardCompatible, 6)]
|
||||
[InlineData(DiffCategory.DatabaseMigration, 10)]
|
||||
[InlineData(DiffCategory.CryptoPayment, 15)]
|
||||
public void CalculateScore_UsesCorrectDiffRisk(DiffCategory category, int expectedDiffRisk)
|
||||
{
|
||||
var input = CreateInput(ServiceTier.Internal, category);
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
result.Breakdown.DiffRisk.Should().Be(expectedDiffRisk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_AddsOperationalContext()
|
||||
{
|
||||
var input = CreateInput(
|
||||
ServiceTier.CustomerFacingNonCritical,
|
||||
DiffCategory.DocsOnly,
|
||||
context: new OperationalContext
|
||||
{
|
||||
HasRecentIncident = true,
|
||||
ErrorBudgetBelow50Percent = true
|
||||
});
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
result.Breakdown.OperationalContext.Should().Be(8); // 5 + 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_SubtractsMitigations()
|
||||
{
|
||||
var input = CreateInput(
|
||||
ServiceTier.CustomerFacingNonCritical,
|
||||
DiffCategory.ApiBackwardCompatible,
|
||||
mitigations: new MitigationFactors
|
||||
{
|
||||
HasFeatureFlag = true,
|
||||
HasCanaryDeployment = true
|
||||
});
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
result.Breakdown.Mitigations.Should().Be(6); // 3 + 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_MinimumIsOne()
|
||||
{
|
||||
var input = CreateInput(
|
||||
ServiceTier.Internal,
|
||||
DiffCategory.DocsOnly,
|
||||
mitigations: new MitigationFactors
|
||||
{
|
||||
HasFeatureFlag = true,
|
||||
HasCanaryDeployment = true,
|
||||
HasHighTestCoverage = true
|
||||
});
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
result.Score.Should().Be(1);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5, GateLevel.G1)]
|
||||
[InlineData(6, GateLevel.G2)]
|
||||
[InlineData(12, GateLevel.G2)]
|
||||
[InlineData(13, GateLevel.G3)]
|
||||
[InlineData(20, GateLevel.G3)]
|
||||
[InlineData(21, GateLevel.G4)]
|
||||
public void CalculateScore_DeterminesCorrectGateLevel(int targetScore, GateLevel expectedGate)
|
||||
{
|
||||
// Use Tier 0 (base=1) + appropriate diff to hit target
|
||||
var diffCategory = targetScore switch
|
||||
{
|
||||
<= 5 => DiffCategory.UiNonCore, // 1 + 3 = 4
|
||||
<= 12 => DiffCategory.ApiBackwardCompatible, // 1 + 6 = 7
|
||||
<= 20 => DiffCategory.InfraNetworking, // 1 + 15 = 16
|
||||
_ => DiffCategory.CryptoPayment // 1 + 15 = 16, add context to get > 20
|
||||
};
|
||||
|
||||
var context = targetScore > 20
|
||||
? new OperationalContext { HasRecentIncident = true, InRestrictedWindow = true }
|
||||
: OperationalContext.Default;
|
||||
|
||||
var input = CreateInput(ServiceTier.Internal, diffCategory, context: context);
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
result.RecommendedGate.Should().Be(expectedGate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_EscalatesGateOnYellowBudget()
|
||||
{
|
||||
var input = CreateInput(
|
||||
ServiceTier.CustomerFacingNonCritical,
|
||||
DiffCategory.ApiBackwardCompatible,
|
||||
context: new OperationalContext { BudgetStatus = BudgetStatus.Yellow });
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
// Base=3 + Diff=6 = 9 → G2, but Yellow escalates G2+ → G3
|
||||
result.RecommendedGate.Should().Be(GateLevel.G3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_EscalatesGateOnRedBudget()
|
||||
{
|
||||
var input = CreateInput(
|
||||
ServiceTier.CustomerFacingNonCritical,
|
||||
DiffCategory.DocsOnly,
|
||||
context: new OperationalContext { BudgetStatus = BudgetStatus.Red });
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
// Base=3 + Diff=1 = 4 → G1, but Red escalates G1+ → G2
|
||||
result.RecommendedGate.Should().Be(GateLevel.G2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_MaxGateOnExhaustedBudget()
|
||||
{
|
||||
var input = CreateInput(
|
||||
ServiceTier.Internal,
|
||||
DiffCategory.DocsOnly,
|
||||
context: new OperationalContext { BudgetStatus = BudgetStatus.Exhausted });
|
||||
|
||||
var result = _scoring.CalculateScore(input);
|
||||
|
||||
result.RecommendedGate.Should().Be(GateLevel.G4);
|
||||
}
|
||||
|
||||
private static RiskScoreInput CreateInput(
|
||||
ServiceTier tier,
|
||||
DiffCategory category,
|
||||
OperationalContext? context = null,
|
||||
MitigationFactors? mitigations = null) => new()
|
||||
{
|
||||
Tier = tier,
|
||||
DiffCategory = category,
|
||||
Context = context ?? OperationalContext.Default,
|
||||
Mitigations = mitigations ?? MitigationFactors.None
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user