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,197 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Policy.Replay;
|
||||
using StellaOps.Policy.Snapshots;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Replay;
|
||||
|
||||
public sealed class ReplayEngineTests
|
||||
{
|
||||
private readonly ICryptoHash _hasher = DefaultCryptoHash.CreateForTests();
|
||||
private readonly InMemorySnapshotStore _snapshotStore = new();
|
||||
private readonly SnapshotService _snapshotService;
|
||||
private readonly ReplayEngine _engine;
|
||||
|
||||
public ReplayEngineTests()
|
||||
{
|
||||
var idGenerator = new SnapshotIdGenerator(_hasher);
|
||||
_snapshotService = new SnapshotService(
|
||||
idGenerator,
|
||||
_snapshotStore,
|
||||
NullLogger<SnapshotService>.Instance);
|
||||
|
||||
var sourceResolver = new KnowledgeSourceResolver(
|
||||
_snapshotStore,
|
||||
NullLogger<KnowledgeSourceResolver>.Instance);
|
||||
|
||||
var verdictComparer = new VerdictComparer();
|
||||
|
||||
_engine = new ReplayEngine(
|
||||
_snapshotService,
|
||||
sourceResolver,
|
||||
verdictComparer,
|
||||
NullLogger<ReplayEngine>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replay_ValidSnapshot_ReturnsResult()
|
||||
{
|
||||
var snapshot = await CreateSnapshotAsync();
|
||||
|
||||
var request = new ReplayRequest
|
||||
{
|
||||
ArtifactDigest = "sha256:test123",
|
||||
SnapshotId = snapshot.SnapshotId
|
||||
};
|
||||
|
||||
var result = await _engine.ReplayAsync(request);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.SnapshotId.Should().Be(snapshot.SnapshotId);
|
||||
result.ReplayedVerdict.Should().NotBeNull();
|
||||
result.ReplayedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replay_NonExistentSnapshot_ReturnsReplayFailed()
|
||||
{
|
||||
var request = new ReplayRequest
|
||||
{
|
||||
ArtifactDigest = "sha256:test123",
|
||||
SnapshotId = "ksm:sha256:nonexistent"
|
||||
};
|
||||
|
||||
var result = await _engine.ReplayAsync(request);
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.ReplayFailed);
|
||||
result.DeltaReport.Should().NotBeNull();
|
||||
result.DeltaReport!.Summary.Should().Contain("not found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replay_NoOriginalVerdict_ReturnsNoComparison()
|
||||
{
|
||||
var snapshot = await CreateSnapshotAsync();
|
||||
|
||||
var request = new ReplayRequest
|
||||
{
|
||||
ArtifactDigest = "sha256:test123",
|
||||
SnapshotId = snapshot.SnapshotId,
|
||||
OriginalVerdictId = null,
|
||||
Options = new ReplayOptions { CompareWithOriginal = true }
|
||||
};
|
||||
|
||||
var result = await _engine.ReplayAsync(request);
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.NoComparison);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replay_SameInputs_ProducesDeterministicResult()
|
||||
{
|
||||
var snapshot = await CreateSnapshotAsync();
|
||||
|
||||
var request = new ReplayRequest
|
||||
{
|
||||
ArtifactDigest = "sha256:determinism-test",
|
||||
SnapshotId = snapshot.SnapshotId
|
||||
};
|
||||
|
||||
// Run multiple times
|
||||
var results = new List<ReplayResult>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
results.Add(await _engine.ReplayAsync(request));
|
||||
}
|
||||
|
||||
// All results should have identical verdicts
|
||||
var firstScore = results[0].ReplayedVerdict.Score;
|
||||
var firstDecision = results[0].ReplayedVerdict.Decision;
|
||||
|
||||
results.Should().AllSatisfy(r =>
|
||||
{
|
||||
r.ReplayedVerdict.Score.Should().Be(firstScore);
|
||||
r.ReplayedVerdict.Decision.Should().Be(firstDecision);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replay_DifferentArtifacts_ProducesDifferentResults()
|
||||
{
|
||||
var snapshot = await CreateSnapshotAsync();
|
||||
|
||||
var request1 = new ReplayRequest
|
||||
{
|
||||
ArtifactDigest = "sha256:artifact-a",
|
||||
SnapshotId = snapshot.SnapshotId
|
||||
};
|
||||
|
||||
var request2 = new ReplayRequest
|
||||
{
|
||||
ArtifactDigest = "sha256:artifact-b",
|
||||
SnapshotId = snapshot.SnapshotId
|
||||
};
|
||||
|
||||
var result1 = await _engine.ReplayAsync(request1);
|
||||
var result2 = await _engine.ReplayAsync(request2);
|
||||
|
||||
// Different inputs may produce different results
|
||||
// (both are valid, just testing they can differ)
|
||||
result1.ReplayedVerdict.ArtifactDigest.Should().NotBe(result2.ReplayedVerdict.ArtifactDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replay_RecordsDuration()
|
||||
{
|
||||
var snapshot = await CreateSnapshotAsync();
|
||||
|
||||
var request = new ReplayRequest
|
||||
{
|
||||
ArtifactDigest = "sha256:test123",
|
||||
SnapshotId = snapshot.SnapshotId
|
||||
};
|
||||
|
||||
var result = await _engine.ReplayAsync(request);
|
||||
|
||||
result.Duration.Should().BeGreaterThan(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replay_WithValidOriginalVerdictId_AttemptsComparison()
|
||||
{
|
||||
var snapshot = await CreateSnapshotAsync();
|
||||
|
||||
var request = new ReplayRequest
|
||||
{
|
||||
ArtifactDigest = "sha256:test123",
|
||||
SnapshotId = snapshot.SnapshotId,
|
||||
OriginalVerdictId = "verdict-not-found",
|
||||
Options = new ReplayOptions { CompareWithOriginal = true }
|
||||
};
|
||||
|
||||
var result = await _engine.ReplayAsync(request);
|
||||
|
||||
// Original verdict not implemented in test, so no comparison
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.NoComparison);
|
||||
}
|
||||
|
||||
private async Task<KnowledgeSnapshotManifest> CreateSnapshotAsync()
|
||||
{
|
||||
var builder = new SnapshotBuilder(_hasher)
|
||||
.WithEngine("stellaops-policy", "1.0.0", "abc123")
|
||||
.WithPolicy("test-policy", "1.0", "sha256:policy123")
|
||||
.WithScoring("test-scoring", "1.0", "sha256:scoring123")
|
||||
.WithSource(new KnowledgeSourceDescriptor
|
||||
{
|
||||
Name = "test-feed",
|
||||
Type = "advisory-feed",
|
||||
Epoch = DateTimeOffset.UtcNow.ToString("o"),
|
||||
Digest = "sha256:feed123",
|
||||
InclusionMode = SourceInclusionMode.Referenced
|
||||
});
|
||||
|
||||
return await _snapshotService.CreateSnapshotAsync(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Replay;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Replay;
|
||||
|
||||
public sealed class ReplayReportTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_CreatesReportWithRequiredFields()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.ExactMatch);
|
||||
|
||||
var report = new ReplayReportBuilder(request, result).Build();
|
||||
|
||||
report.ReportId.Should().StartWith("rpt:");
|
||||
report.ArtifactDigest.Should().Be(request.ArtifactDigest);
|
||||
report.SnapshotId.Should().Be(request.SnapshotId);
|
||||
report.MatchStatus.Should().Be(ReplayMatchStatus.ExactMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ExactMatch_SetsDeterministicTrue()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.ExactMatch);
|
||||
|
||||
var report = new ReplayReportBuilder(request, result).Build();
|
||||
|
||||
report.IsDeterministic.Should().BeTrue();
|
||||
report.DeterminismConfidence.Should().Be(1.0m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_Mismatch_SetsDeterministicFalse()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.Mismatch);
|
||||
|
||||
var report = new ReplayReportBuilder(request, result).Build();
|
||||
|
||||
report.IsDeterministic.Should().BeFalse();
|
||||
report.DeterminismConfidence.Should().Be(0.0m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MatchWithinTolerance_SetsHighConfidence()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.MatchWithinTolerance);
|
||||
|
||||
var report = new ReplayReportBuilder(request, result).Build();
|
||||
|
||||
report.IsDeterministic.Should().BeFalse();
|
||||
report.DeterminismConfidence.Should().Be(0.9m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NoComparison_SetsMediumConfidence()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.NoComparison);
|
||||
|
||||
var report = new ReplayReportBuilder(request, result).Build();
|
||||
|
||||
report.DeterminismConfidence.Should().Be(0.5m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddRecommendation_AddsToList()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.ExactMatch);
|
||||
|
||||
var report = new ReplayReportBuilder(request, result)
|
||||
.AddRecommendation("Test recommendation")
|
||||
.Build();
|
||||
|
||||
report.Recommendations.Should().Contain("Test recommendation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddRecommendationsFromResult_MismatchAddsReviewRecommendation()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.Mismatch);
|
||||
|
||||
var report = new ReplayReportBuilder(request, result)
|
||||
.AddRecommendationsFromResult()
|
||||
.Build();
|
||||
|
||||
report.Recommendations.Should().Contain(r => r.Contains("delta report"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddRecommendationsFromResult_FailedAddsSnapshotRecommendation()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.ReplayFailed);
|
||||
|
||||
var report = new ReplayReportBuilder(request, result)
|
||||
.AddRecommendationsFromResult()
|
||||
.Build();
|
||||
|
||||
report.Recommendations.Should().Contain(r => r.Contains("snapshot"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_IncludesTiming()
|
||||
{
|
||||
var request = CreateRequest();
|
||||
var result = CreateResult(ReplayMatchStatus.ExactMatch) with
|
||||
{
|
||||
Duration = TimeSpan.FromMilliseconds(150)
|
||||
};
|
||||
|
||||
var report = new ReplayReportBuilder(request, result).Build();
|
||||
|
||||
report.Timing.TotalDuration.Should().Be(TimeSpan.FromMilliseconds(150));
|
||||
}
|
||||
|
||||
private static ReplayRequest CreateRequest() => new()
|
||||
{
|
||||
ArtifactDigest = "sha256:test123",
|
||||
SnapshotId = "ksm:sha256:snapshot123",
|
||||
OriginalVerdictId = "verdict-001"
|
||||
};
|
||||
|
||||
private static ReplayResult CreateResult(ReplayMatchStatus status) => new()
|
||||
{
|
||||
MatchStatus = status,
|
||||
ReplayedVerdict = ReplayedVerdict.Empty with { ArtifactDigest = "sha256:test123" },
|
||||
SnapshotId = "ksm:sha256:snapshot123",
|
||||
ReplayedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Replay;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Replay;
|
||||
|
||||
public sealed class VerdictComparerTests
|
||||
{
|
||||
private readonly VerdictComparer _comparer = new();
|
||||
|
||||
[Fact]
|
||||
public void Compare_IdenticalVerdicts_ReturnsExactMatch()
|
||||
{
|
||||
var verdict = CreateVerdict(decision: ReplayDecision.Pass, score: 85.5m);
|
||||
|
||||
var result = _comparer.Compare(verdict, verdict, VerdictComparisonOptions.Default);
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.ExactMatch);
|
||||
result.IsDeterministic.Should().BeTrue();
|
||||
result.DeterminismConfidence.Should().Be(1.0m);
|
||||
result.Differences.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_DifferentDecisions_ReturnsMismatch()
|
||||
{
|
||||
var original = CreateVerdict(decision: ReplayDecision.Pass);
|
||||
var replayed = CreateVerdict(decision: ReplayDecision.Fail);
|
||||
|
||||
var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default);
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.Mismatch);
|
||||
result.IsDeterministic.Should().BeFalse();
|
||||
result.Differences.Should().Contain(d => d.Field == "Decision");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_ScoreWithinTolerance_ReturnsMatchWithinTolerance()
|
||||
{
|
||||
var original = CreateVerdict(score: 85.5000m);
|
||||
var replayed = CreateVerdict(score: 85.5005m);
|
||||
|
||||
var result = _comparer.Compare(replayed, original,
|
||||
new VerdictComparisonOptions { ScoreTolerance = 0.001m, TreatMinorAsMatch = true });
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.MatchWithinTolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_ScoreBeyondTolerance_ReturnsMismatch()
|
||||
{
|
||||
var original = CreateVerdict(score: 85.5m);
|
||||
var replayed = CreateVerdict(score: 86.0m);
|
||||
|
||||
var result = _comparer.Compare(replayed, original,
|
||||
new VerdictComparisonOptions { ScoreTolerance = 0.001m, CriticalScoreTolerance = 0.1m });
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.Mismatch);
|
||||
result.Differences.Should().Contain(d => d.Field == "Score");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_DifferentFindings_DetectsAddedAndRemoved()
|
||||
{
|
||||
var original = CreateVerdictWithFindings("CVE-2024-001", "CVE-2024-002");
|
||||
var replayed = CreateVerdictWithFindings("CVE-2024-001", "CVE-2024-003");
|
||||
|
||||
var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default);
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.Mismatch);
|
||||
result.Differences.Should().Contain(d => d.Field == "Finding:CVE-2024-002" && d.ReplayedValue == "absent");
|
||||
result.Differences.Should().Contain(d => d.Field == "Finding:CVE-2024-003" && d.OriginalValue == "absent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_SameFindings_DifferentOrder_ReturnsMatch()
|
||||
{
|
||||
var original = CreateVerdictWithFindings("CVE-2024-001", "CVE-2024-002", "CVE-2024-003");
|
||||
var replayed = CreateVerdictWithFindings("CVE-2024-003", "CVE-2024-001", "CVE-2024-002");
|
||||
|
||||
var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default);
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.ExactMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_ExtraFindings_DetectsAdditions()
|
||||
{
|
||||
var original = CreateVerdictWithFindings("CVE-2024-001");
|
||||
var replayed = CreateVerdictWithFindings("CVE-2024-001", "CVE-2024-002");
|
||||
|
||||
var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default);
|
||||
|
||||
result.MatchStatus.Should().Be(ReplayMatchStatus.Mismatch);
|
||||
result.Differences.Should().ContainSingle(d => d.Field == "Finding:CVE-2024-002");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_CalculatesCorrectConfidence()
|
||||
{
|
||||
var original = CreateVerdict(decision: ReplayDecision.Pass, score: 85.0m);
|
||||
var replayed = CreateVerdict(decision: ReplayDecision.Fail, score: 75.0m);
|
||||
|
||||
var result = _comparer.Compare(replayed, original, VerdictComparisonOptions.Default);
|
||||
|
||||
result.DeterminismConfidence.Should().BeLessThan(1.0m);
|
||||
result.DeterminismConfidence.Should().BeGreaterThanOrEqualTo(0m);
|
||||
}
|
||||
|
||||
private static ReplayedVerdict CreateVerdict(
|
||||
ReplayDecision decision = ReplayDecision.Pass,
|
||||
decimal score = 85.0m) => new()
|
||||
{
|
||||
ArtifactDigest = "sha256:test123",
|
||||
Decision = decision,
|
||||
Score = score,
|
||||
FindingIds = []
|
||||
};
|
||||
|
||||
private static ReplayedVerdict CreateVerdictWithFindings(params string[] findingIds) => new()
|
||||
{
|
||||
ArtifactDigest = "sha256:test123",
|
||||
Decision = ReplayDecision.Pass,
|
||||
Score = 85.0m,
|
||||
FindingIds = findingIds.ToList()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user