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

@@ -0,0 +1,25 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,155 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Authority.Core.Verdicts;
using Xunit;
namespace StellaOps.Authority.Core.Tests.Verdicts;
public sealed class InMemoryVerdictManifestStoreTests
{
private readonly InMemoryVerdictManifestStore _store = new();
[Fact]
public async Task StoreAndRetrieve_ByManifestId()
{
var manifest = CreateManifest("manifest-1", "tenant-1");
await _store.StoreAsync(manifest);
var retrieved = await _store.GetByIdAsync("tenant-1", "manifest-1");
retrieved.Should().NotBeNull();
retrieved!.ManifestId.Should().Be("manifest-1");
retrieved.Tenant.Should().Be("tenant-1");
}
[Fact]
public async Task GetByScope_ReturnsLatest()
{
var older = CreateManifest("m1", "t", evaluatedAt: DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
var newer = CreateManifest("m2", "t", evaluatedAt: DateTimeOffset.Parse("2025-01-02T00:00:00Z"));
await _store.StoreAsync(older);
await _store.StoreAsync(newer);
var result = await _store.GetByScopeAsync("t", "sha256:asset", "CVE-2024-1234");
result.Should().NotBeNull();
result!.ManifestId.Should().Be("m2");
}
[Fact]
public async Task GetByScope_FiltersOnPolicyAndLattice()
{
var m1 = CreateManifest("m1", "t", policyHash: "p1", latticeVersion: "v1");
var m2 = CreateManifest("m2", "t", policyHash: "p2", latticeVersion: "v1");
await _store.StoreAsync(m1);
await _store.StoreAsync(m2);
var result = await _store.GetByScopeAsync("t", "sha256:asset", "CVE-2024-1234", policyHash: "p1");
result.Should().NotBeNull();
result!.ManifestId.Should().Be("m1");
}
[Fact]
public async Task ListByPolicy_Paginates()
{
for (var i = 0; i < 5; i++)
{
var manifest = CreateManifest($"m{i}", "t", policyHash: "p1", latticeVersion: "v1",
evaluatedAt: DateTimeOffset.UtcNow.AddMinutes(-i));
await _store.StoreAsync(manifest);
}
var page1 = await _store.ListByPolicyAsync("t", "p1", "v1", limit: 2);
page1.Manifests.Should().HaveCount(2);
page1.NextPageToken.Should().NotBeNull();
var page2 = await _store.ListByPolicyAsync("t", "p1", "v1", limit: 2, pageToken: page1.NextPageToken);
page2.Manifests.Should().HaveCount(2);
page2.NextPageToken.Should().NotBeNull();
var page3 = await _store.ListByPolicyAsync("t", "p1", "v1", limit: 2, pageToken: page2.NextPageToken);
page3.Manifests.Should().HaveCount(1);
page3.NextPageToken.Should().BeNull();
}
[Fact]
public async Task Delete_RemovesManifest()
{
var manifest = CreateManifest("m1", "t");
await _store.StoreAsync(manifest);
var deleted = await _store.DeleteAsync("t", "m1");
deleted.Should().BeTrue();
var retrieved = await _store.GetByIdAsync("t", "m1");
retrieved.Should().BeNull();
}
[Fact]
public async Task Delete_ReturnsFalseWhenNotFound()
{
var deleted = await _store.DeleteAsync("t", "nonexistent");
deleted.Should().BeFalse();
}
[Fact]
public async Task TenantIsolation_Works()
{
var m1 = CreateManifest("shared-id", "tenant-a");
var m2 = CreateManifest("shared-id", "tenant-b");
await _store.StoreAsync(m1);
await _store.StoreAsync(m2);
var fromA = await _store.GetByIdAsync("tenant-a", "shared-id");
var fromB = await _store.GetByIdAsync("tenant-b", "shared-id");
fromA.Should().NotBeNull();
fromB.Should().NotBeNull();
fromA!.Tenant.Should().Be("tenant-a");
fromB!.Tenant.Should().Be("tenant-b");
_store.Count.Should().Be(2);
}
private static VerdictManifest CreateManifest(
string manifestId,
string tenant,
string assetDigest = "sha256:asset",
string vulnerabilityId = "CVE-2024-1234",
string policyHash = "sha256:policy",
string latticeVersion = "1.0.0",
DateTimeOffset? evaluatedAt = null)
{
return new VerdictManifest
{
ManifestId = manifestId,
Tenant = tenant,
AssetDigest = assetDigest,
VulnerabilityId = vulnerabilityId,
Inputs = new VerdictInputs
{
SbomDigests = ImmutableArray.Create("sha256:sbom"),
VulnFeedSnapshotIds = ImmutableArray.Create("feed-1"),
VexDocumentDigests = ImmutableArray.Create("sha256:vex"),
ReachabilityGraphIds = ImmutableArray<string>.Empty,
ClockCutoff = DateTimeOffset.UtcNow,
},
Result = new VerdictResult
{
Status = VexStatus.NotAffected,
Confidence = 0.85,
Explanations = ImmutableArray<VerdictExplanation>.Empty,
EvidenceRefs = ImmutableArray<string>.Empty,
},
PolicyHash = policyHash,
LatticeVersion = latticeVersion,
EvaluatedAt = evaluatedAt ?? DateTimeOffset.UtcNow,
ManifestDigest = $"sha256:{manifestId}",
};
}
}

View File

@@ -0,0 +1,165 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Authority.Core.Verdicts;
using Xunit;
namespace StellaOps.Authority.Core.Tests.Verdicts;
public sealed class VerdictManifestBuilderTests
{
[Fact]
public void Build_CreatesValidManifest()
{
var builder = new VerdictManifestBuilder(() => "test-manifest-id")
.WithTenant("tenant-1")
.WithAsset("sha256:abc123", "CVE-2024-1234")
.WithInputs(
sbomDigests: new[] { "sha256:sbom1" },
vulnFeedSnapshotIds: new[] { "feed-snapshot-1" },
vexDocumentDigests: new[] { "sha256:vex1" },
clockCutoff: DateTimeOffset.Parse("2025-01-01T00:00:00Z"))
.WithResult(
status: VexStatus.NotAffected,
confidence: 0.85,
explanations: new[]
{
new VerdictExplanation
{
SourceId = "vendor-a",
Reason = "Official vendor VEX",
ProvenanceScore = 0.9,
CoverageScore = 0.8,
ReplayabilityScore = 0.7,
StrengthMultiplier = 1.0,
FreshnessMultiplier = 0.95,
ClaimScore = 0.85,
AssertedStatus = VexStatus.NotAffected,
Accepted = true,
},
})
.WithPolicy("sha256:policy123", "1.0.0")
.WithClock(DateTimeOffset.Parse("2025-01-01T12:00:00Z"));
var manifest = builder.Build();
manifest.ManifestId.Should().Be("test-manifest-id");
manifest.Tenant.Should().Be("tenant-1");
manifest.AssetDigest.Should().Be("sha256:abc123");
manifest.VulnerabilityId.Should().Be("CVE-2024-1234");
manifest.Result.Status.Should().Be(VexStatus.NotAffected);
manifest.Result.Confidence.Should().Be(0.85);
manifest.ManifestDigest.Should().StartWith("sha256:");
}
[Fact]
public void Build_IsDeterministic()
{
var clock = DateTimeOffset.Parse("2025-01-01T12:00:00Z");
var inputClock = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
VerdictManifest BuildManifest(int seed)
{
return new VerdictManifestBuilder(() => "fixed-id")
.WithTenant("tenant")
.WithAsset("sha256:asset", "CVE-2024-0001")
.WithInputs(
sbomDigests: new[] { "sha256:sbom" },
vulnFeedSnapshotIds: new[] { "feed-1" },
vexDocumentDigests: new[] { "sha256:vex" },
clockCutoff: inputClock)
.WithResult(
status: VexStatus.Fixed,
confidence: 0.9,
explanations: new[]
{
new VerdictExplanation
{
SourceId = "source",
Reason = "Fixed",
ProvenanceScore = 0.9,
CoverageScore = 0.9,
ReplayabilityScore = 0.9,
StrengthMultiplier = 1.0,
FreshnessMultiplier = 1.0,
ClaimScore = 0.9,
AssertedStatus = VexStatus.Fixed,
Accepted = true,
},
})
.WithPolicy("sha256:policy", "1.0")
.WithClock(clock)
.Build();
}
var first = BuildManifest(1);
for (var i = 0; i < 100; i++)
{
var next = BuildManifest(i);
next.ManifestDigest.Should().Be(first.ManifestDigest, "manifests should be deterministic");
}
}
[Fact]
public void Build_SortsInputsDeterministically()
{
var clock = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var manifestA = new VerdictManifestBuilder(() => "id")
.WithTenant("t")
.WithAsset("sha256:a", "CVE-1")
.WithInputs(
sbomDigests: new[] { "c", "a", "b" },
vulnFeedSnapshotIds: new[] { "z", "y" },
vexDocumentDigests: new[] { "3", "1", "2" },
clockCutoff: clock)
.WithResult(VexStatus.Affected, 0.5, Enumerable.Empty<VerdictExplanation>())
.WithPolicy("p", "v")
.WithClock(clock)
.Build();
var manifestB = new VerdictManifestBuilder(() => "id")
.WithTenant("t")
.WithAsset("sha256:a", "CVE-1")
.WithInputs(
sbomDigests: new[] { "b", "c", "a" },
vulnFeedSnapshotIds: new[] { "y", "z" },
vexDocumentDigests: new[] { "2", "3", "1" },
clockCutoff: clock)
.WithResult(VexStatus.Affected, 0.5, Enumerable.Empty<VerdictExplanation>())
.WithPolicy("p", "v")
.WithClock(clock)
.Build();
manifestA.ManifestDigest.Should().Be(manifestB.ManifestDigest);
manifestA.Inputs.SbomDigests.Should().Equal("a", "b", "c");
}
[Fact]
public void Build_ThrowsOnMissingRequiredFields()
{
var builder = new VerdictManifestBuilder();
var act = () => builder.Build();
act.Should().Throw<InvalidOperationException>()
.WithMessage("*validation failed*");
}
[Fact]
public void Build_NormalizesVulnerabilityIdToUpperCase()
{
var manifest = new VerdictManifestBuilder(() => "id")
.WithTenant("t")
.WithAsset("sha256:a", "cve-2024-1234")
.WithInputs(
sbomDigests: new[] { "sha256:s" },
vulnFeedSnapshotIds: new[] { "f" },
vexDocumentDigests: new[] { "v" },
clockCutoff: DateTimeOffset.UtcNow)
.WithResult(VexStatus.Affected, 0.5, Enumerable.Empty<VerdictExplanation>())
.WithPolicy("p", "v")
.Build();
manifest.VulnerabilityId.Should().Be("CVE-2024-1234");
}
}

View File

@@ -0,0 +1,122 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Authority.Core.Verdicts;
using Xunit;
namespace StellaOps.Authority.Core.Tests.Verdicts;
public sealed class VerdictManifestSerializerTests
{
[Fact]
public void Serialize_ProducesValidJson()
{
var manifest = CreateTestManifest();
var json = VerdictManifestSerializer.Serialize(manifest);
json.Should().Contain("\"manifest_id\"");
json.Should().Contain("\"tenant\"");
json.Should().Contain("\"not_affected\"");
json.Should().NotContain("\"ManifestId\""); // Should use snake_case
}
[Fact]
public void SerializeDeserialize_RoundTrips()
{
var manifest = CreateTestManifest();
var json = VerdictManifestSerializer.Serialize(manifest);
var deserialized = VerdictManifestSerializer.Deserialize(json);
deserialized.Should().NotBeNull();
deserialized!.ManifestId.Should().Be(manifest.ManifestId);
deserialized.Result.Status.Should().Be(manifest.Result.Status);
deserialized.Result.Confidence.Should().Be(manifest.Result.Confidence);
}
[Fact]
public void ComputeDigest_IsDeterministic()
{
var manifest = CreateTestManifest();
var digest1 = VerdictManifestSerializer.ComputeDigest(manifest);
var digest2 = VerdictManifestSerializer.ComputeDigest(manifest);
digest1.Should().Be(digest2);
digest1.Should().StartWith("sha256:");
}
[Fact]
public void ComputeDigest_ChangesWithContent()
{
var manifest1 = CreateTestManifest();
var manifest2 = manifest1 with
{
Result = manifest1.Result with { Confidence = 0.5 }
};
var digest1 = VerdictManifestSerializer.ComputeDigest(manifest1);
var digest2 = VerdictManifestSerializer.ComputeDigest(manifest2);
digest1.Should().NotBe(digest2);
}
[Fact]
public void ComputeDigest_IgnoresSignatureFields()
{
var manifest1 = CreateTestManifest();
var manifest2 = manifest1 with
{
SignatureBase64 = "some-signature",
RekorLogId = "some-log-id"
};
var digest1 = VerdictManifestSerializer.ComputeDigest(manifest1);
var digest2 = VerdictManifestSerializer.ComputeDigest(manifest2);
digest1.Should().Be(digest2);
}
private static VerdictManifest CreateTestManifest()
{
return new VerdictManifest
{
ManifestId = "test-id",
Tenant = "test-tenant",
AssetDigest = "sha256:asset123",
VulnerabilityId = "CVE-2024-1234",
Inputs = new VerdictInputs
{
SbomDigests = ImmutableArray.Create("sha256:sbom1"),
VulnFeedSnapshotIds = ImmutableArray.Create("feed-1"),
VexDocumentDigests = ImmutableArray.Create("sha256:vex1"),
ReachabilityGraphIds = ImmutableArray<string>.Empty,
ClockCutoff = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
},
Result = new VerdictResult
{
Status = VexStatus.NotAffected,
Confidence = 0.85,
Explanations = ImmutableArray.Create(
new VerdictExplanation
{
SourceId = "vendor-a",
Reason = "Official vendor statement",
ProvenanceScore = 0.9,
CoverageScore = 0.8,
ReplayabilityScore = 0.7,
StrengthMultiplier = 1.0,
FreshnessMultiplier = 0.95,
ClaimScore = 0.85,
AssertedStatus = VexStatus.NotAffected,
Accepted = true,
}),
EvidenceRefs = ImmutableArray.Create("evidence-1"),
},
PolicyHash = "sha256:policy123",
LatticeVersion = "1.0.0",
EvaluatedAt = DateTimeOffset.Parse("2025-01-01T12:00:00Z"),
ManifestDigest = "sha256:placeholder",
};
}
}