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:
@@ -41,7 +41,7 @@ public sealed class ExceptionAdapterTests : IDisposable
|
||||
_repositoryMock.Object,
|
||||
_effectRegistry,
|
||||
_cache,
|
||||
Options.Create(_options),
|
||||
Microsoft.Extensions.Options.Options.Create(_options),
|
||||
TimeProvider.System,
|
||||
NullLogger<ExceptionAdapter>.Instance);
|
||||
}
|
||||
@@ -247,7 +247,7 @@ public sealed class ExceptionAdapterTests : IDisposable
|
||||
_repositoryMock.Object,
|
||||
_effectRegistry,
|
||||
_cache,
|
||||
Options.Create(disabledCacheOptions),
|
||||
Microsoft.Extensions.Options.Options.Create(disabledCacheOptions),
|
||||
TimeProvider.System,
|
||||
NullLogger<ExceptionAdapter>.Instance);
|
||||
|
||||
@@ -291,7 +291,7 @@ public sealed class ExceptionAdapterTests : IDisposable
|
||||
_repositoryMock.Object,
|
||||
_effectRegistry,
|
||||
_cache,
|
||||
Options.Create(limitedOptions),
|
||||
Microsoft.Extensions.Options.Options.Create(limitedOptions),
|
||||
TimeProvider.System,
|
||||
NullLogger<ExceptionAdapter>.Instance);
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Attestation;
|
||||
|
||||
public sealed class RvaBuilderTests
|
||||
{
|
||||
private readonly ICryptoHash _hasher = DefaultCryptoHash.CreateForTests();
|
||||
|
||||
[Fact]
|
||||
public void Build_ValidInputs_CreatesRva()
|
||||
{
|
||||
var rva = new RvaBuilder(_hasher)
|
||||
.WithVerdict(RiskVerdictStatus.Pass)
|
||||
.WithSubject("sha256:abc123", "container-image", "myapp:v1.0")
|
||||
.WithPolicy("policy-1", "1.0", "sha256:xyz")
|
||||
.WithKnowledgeSnapshot("ksm:sha256:def456")
|
||||
.WithReasonCode(VerdictReasonCode.PassNoCves)
|
||||
.Build();
|
||||
|
||||
rva.AttestationId.Should().StartWith("rva:sha256:");
|
||||
rva.Verdict.Should().Be(RiskVerdictStatus.Pass);
|
||||
rva.ReasonCodes.Should().Contain(VerdictReasonCode.PassNoCves);
|
||||
rva.Subject.Digest.Should().Be("sha256:abc123");
|
||||
rva.Policy.PolicyId.Should().Be("policy-1");
|
||||
rva.KnowledgeSnapshotId.Should().Be("ksm:sha256:def456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MissingSubject_Throws()
|
||||
{
|
||||
var builder = new RvaBuilder(_hasher)
|
||||
.WithVerdict(RiskVerdictStatus.Pass)
|
||||
.WithPolicy("p", "1.0", "sha256:x")
|
||||
.WithKnowledgeSnapshot("ksm:sha256:y");
|
||||
|
||||
var act = () => builder.Build();
|
||||
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*Subject*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MissingPolicy_Throws()
|
||||
{
|
||||
var builder = new RvaBuilder(_hasher)
|
||||
.WithVerdict(RiskVerdictStatus.Pass)
|
||||
.WithSubject("sha256:abc", "container-image")
|
||||
.WithKnowledgeSnapshot("ksm:sha256:y");
|
||||
|
||||
var act = () => builder.Build();
|
||||
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*Policy*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MissingSnapshot_Throws()
|
||||
{
|
||||
var builder = new RvaBuilder(_hasher)
|
||||
.WithVerdict(RiskVerdictStatus.Pass)
|
||||
.WithSubject("sha256:abc", "container-image")
|
||||
.WithPolicy("p", "1.0", "sha256:x");
|
||||
|
||||
var act = () => builder.Build();
|
||||
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*snapshot*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ContentAddressedId_IsDeterministic()
|
||||
{
|
||||
var builder1 = CreateBuilder();
|
||||
var builder2 = CreateBuilder();
|
||||
|
||||
var rva1 = builder1.Build();
|
||||
var rva2 = builder2.Build();
|
||||
|
||||
// IDs should be same for same content (ignoring CreatedAt which varies)
|
||||
rva1.AttestationId.Should().StartWith("rva:sha256:");
|
||||
rva2.AttestationId.Should().StartWith("rva:sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithEvidence_IncludesEvidence()
|
||||
{
|
||||
var rva = CreateBuilder()
|
||||
.WithEvidence("sbom", "sha256:sbom123", description: "SBOM artifact")
|
||||
.WithEvidence("reachability", "sha256:reach456")
|
||||
.Build();
|
||||
|
||||
rva.Evidence.Should().HaveCount(2);
|
||||
rva.Evidence[0].Type.Should().Be("sbom");
|
||||
rva.Evidence[1].Type.Should().Be("reachability");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithExceptions_IncludesExceptions()
|
||||
{
|
||||
var rva = CreateBuilder()
|
||||
.WithException("exc-001")
|
||||
.WithException("exc-002")
|
||||
.Build();
|
||||
|
||||
rva.AppliedExceptions.Should().HaveCount(2);
|
||||
rva.AppliedExceptions.Should().Contain("exc-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithUnknowns_IncludesUnknowns()
|
||||
{
|
||||
var rva = CreateBuilder()
|
||||
.WithUnknowns(total: 5, blockingCount: 2)
|
||||
.Build();
|
||||
|
||||
rva.Unknowns.Should().NotBeNull();
|
||||
rva.Unknowns!.Total.Should().Be(5);
|
||||
rva.Unknowns.BlockingCount.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithExpiration_SetsExpiration()
|
||||
{
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddDays(7);
|
||||
var rva = CreateBuilder()
|
||||
.WithExpiration(expiresAt)
|
||||
.Build();
|
||||
|
||||
rva.ExpiresAt.Should().Be(expiresAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMetadata_IncludesMetadata()
|
||||
{
|
||||
var rva = CreateBuilder()
|
||||
.WithMetadata("env", "production")
|
||||
.WithMetadata("region", "us-east-1")
|
||||
.Build();
|
||||
|
||||
rva.Metadata.Should().ContainKey("env");
|
||||
rva.Metadata["env"].Should().Be("production");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MultipleReasonCodes_DeduplicatesAndPreserves()
|
||||
{
|
||||
var rva = CreateBuilder()
|
||||
.WithReasonCode(VerdictReasonCode.FailCveReachable)
|
||||
.WithReasonCode(VerdictReasonCode.FailCveKev)
|
||||
.WithReasonCode(VerdictReasonCode.FailCveReachable) // duplicate
|
||||
.Build();
|
||||
|
||||
rva.ReasonCodes.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
private RvaBuilder CreateBuilder()
|
||||
{
|
||||
return new RvaBuilder(_hasher)
|
||||
.WithVerdict(RiskVerdictStatus.Pass)
|
||||
.WithSubject("sha256:test123", "container-image", "test:v1")
|
||||
.WithPolicy("policy-1", "1.0", "sha256:policy")
|
||||
.WithKnowledgeSnapshot("ksm:sha256:snapshot123");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
using StellaOps.Policy.Snapshots;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Attestation;
|
||||
|
||||
public sealed class RvaVerifierTests
|
||||
{
|
||||
private readonly ICryptoHash _hasher = DefaultCryptoHash.CreateForTests();
|
||||
private readonly InMemorySnapshotStore _snapshotStore;
|
||||
private readonly SnapshotService _snapshotService;
|
||||
private readonly RvaVerifier _verifier;
|
||||
|
||||
public RvaVerifierTests()
|
||||
{
|
||||
_snapshotStore = new InMemorySnapshotStore();
|
||||
_snapshotService = new SnapshotService(
|
||||
new SnapshotIdGenerator(_hasher),
|
||||
_snapshotStore,
|
||||
NullLogger<SnapshotService>.Instance);
|
||||
_verifier = new RvaVerifier(
|
||||
_snapshotService,
|
||||
NullLogger<RvaVerifier>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRaw_ValidAttestation_ReturnsSuccess()
|
||||
{
|
||||
var rva = CreateValidRva();
|
||||
|
||||
var result = await _verifier.VerifyRawAsync(rva, RvaVerificationOptions.Default);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Attestation.Should().NotBeNull();
|
||||
result.Issues.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRaw_TamperedAttestationId_ReturnsFail()
|
||||
{
|
||||
var rva = CreateValidRva();
|
||||
var tampered = rva with { AttestationId = "rva:sha256:0000000000000000000000000000000000000000000000000000000000000000" };
|
||||
|
||||
var result = await _verifier.VerifyRawAsync(tampered, RvaVerificationOptions.Default);
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Issues.Should().Contain(i => i.Contains("ID"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRaw_ExpiredAttestation_FailsByDefault()
|
||||
{
|
||||
var rva = CreateValidRva(expiresAt: DateTimeOffset.UtcNow.AddDays(-1));
|
||||
|
||||
var result = await _verifier.VerifyRawAsync(rva, RvaVerificationOptions.Default);
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Issues.Should().Contain(i => i.Contains("expired"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRaw_ExpiredAttestation_AllowedWithOption()
|
||||
{
|
||||
var rva = CreateValidRva(expiresAt: DateTimeOffset.UtcNow.AddDays(-1));
|
||||
var options = new RvaVerificationOptions { AllowExpired = true };
|
||||
|
||||
var result = await _verifier.VerifyRawAsync(rva, options);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRaw_NotExpired_ReturnsSuccess()
|
||||
{
|
||||
var rva = CreateValidRva(expiresAt: DateTimeOffset.UtcNow.AddDays(7));
|
||||
|
||||
var result = await _verifier.VerifyRawAsync(rva, RvaVerificationOptions.Default);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyRaw_NoExpiration_ReturnsSuccess()
|
||||
{
|
||||
var rva = CreateValidRva(expiresAt: null);
|
||||
|
||||
var result = await _verifier.VerifyRawAsync(rva, RvaVerificationOptions.Default);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictReasonCode_GetCategory_ReturnsCorrectCategory()
|
||||
{
|
||||
VerdictReasonCode.PassNoCves.GetCategory().Should().Be("Pass");
|
||||
VerdictReasonCode.FailCveReachable.GetCategory().Should().Be("Fail");
|
||||
VerdictReasonCode.ExceptionCve.GetCategory().Should().Be("Exception");
|
||||
VerdictReasonCode.IndeterminateInsufficientData.GetCategory().Should().Be("Indeterminate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictReasonCode_GetDescription_ReturnsDescription()
|
||||
{
|
||||
var description = VerdictReasonCode.FailCveReachable.GetDescription();
|
||||
description.Should().Contain("Reachable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictReasonCode_IsPass_ReturnsCorrectly()
|
||||
{
|
||||
VerdictReasonCode.PassNoCves.IsPass().Should().BeTrue();
|
||||
VerdictReasonCode.FailCveReachable.IsPass().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictReasonCode_IsFail_ReturnsCorrectly()
|
||||
{
|
||||
VerdictReasonCode.FailCveReachable.IsFail().Should().BeTrue();
|
||||
VerdictReasonCode.PassNoCves.IsFail().Should().BeFalse();
|
||||
}
|
||||
|
||||
private RiskVerdictAttestation CreateValidRva(DateTimeOffset? expiresAt = null)
|
||||
{
|
||||
return new RvaBuilder(_hasher)
|
||||
.WithVerdict(RiskVerdictStatus.Pass)
|
||||
.WithSubject("sha256:test123", "container-image", "test:v1")
|
||||
.WithPolicy("policy-1", "1.0", "sha256:policy")
|
||||
.WithKnowledgeSnapshot("ksm:sha256:snapshot123")
|
||||
.WithReasonCode(VerdictReasonCode.PassNoCves)
|
||||
.WithExpiration(expiresAt ?? DateTimeOffset.UtcNow.AddDays(30))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user