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:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -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);

View File

@@ -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");
}
}

View File

@@ -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();
}
}